Upgrade to ExtJS 4.0.2 - Released 06/09/2011
[extjs.git] / src / chart / series / Gauge.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.Gauge
17  * @extends Ext.chart.series.Series
18  * 
19  * Creates a Gauge Chart. Gauge Charts are used to show progress in a certain variable. There are two ways of using the Gauge chart.
20  * One is setting a store element into the Gauge and selecting the field to be used from that store. Another one is instanciating the
21  * visualization and using the `setValue` method to adjust the value you want.
22  *
23  * A chart/series configuration for the Gauge visualization could look like this:
24  * 
25  *     {
26  *         xtype: 'chart',
27  *         store: store,
28  *         axes: [{
29  *             type: 'gauge',
30  *             position: 'gauge',
31  *             minimum: 0,
32  *             maximum: 100,
33  *             steps: 10,
34  *             margin: -10
35  *         }],
36  *         series: [{
37  *             type: 'gauge',
38  *             field: 'data1',
39  *             donut: false,
40  *             colorSet: ['#F49D10', '#ddd']
41  *         }]
42  *     }
43  * 
44  * In this configuration we create a special Gauge axis to be used with the gauge visualization (describing half-circle markers), and also we're
45  * setting a maximum, minimum and steps configuration options into the axis. The Gauge series configuration contains the store field to be bound to
46  * the visual display and the color set to be used with the visualization.
47  * 
48  * @xtype gauge
49  */
50 Ext.define('Ext.chart.series.Gauge', {
51
52     /* Begin Definitions */
53
54     extend: 'Ext.chart.series.Series',
55
56     /* End Definitions */
57
58     type: "gauge",
59     alias: 'series.gauge',
60
61     rad: Math.PI / 180,
62
63     /**
64      * @cfg {Number} highlightDuration
65      * The duration for the pie slice highlight effect.
66      */
67     highlightDuration: 150,
68
69     /**
70      * @cfg {String} angleField
71      * The store record field name to be used for the pie angles.
72      * The values bound to this field name must be positive real numbers.
73      * This parameter is required.
74      */
75     angleField: false,
76
77     /**
78      * @cfg {Boolean} needle
79      * Use the Gauge Series as an area series or add a needle to it. Default's false.
80      */
81     needle: false,
82     
83     /**
84      * @cfg {Boolean|Number} donut
85      * Use the entire disk or just a fraction of it for the gauge. Default's false.
86      */
87     donut: false,
88
89     /**
90      * @cfg {Boolean} showInLegend
91      * Whether to add the pie chart elements as legend items. Default's false.
92      */
93     showInLegend: false,
94
95     /**
96      * @cfg {Object} style
97      * An object containing styles for overriding series styles from Theming.
98      */
99     style: {},
100     
101     constructor: function(config) {
102         this.callParent(arguments);
103         var me = this,
104             chart = me.chart,
105             surface = chart.surface,
106             store = chart.store,
107             shadow = chart.shadow, i, l, cfg;
108         Ext.apply(me, config, {
109             shadowAttributes: [{
110                 "stroke-width": 6,
111                 "stroke-opacity": 1,
112                 stroke: 'rgb(200, 200, 200)',
113                 translate: {
114                     x: 1.2,
115                     y: 2
116                 }
117             },
118             {
119                 "stroke-width": 4,
120                 "stroke-opacity": 1,
121                 stroke: 'rgb(150, 150, 150)',
122                 translate: {
123                     x: 0.9,
124                     y: 1.5
125                 }
126             },
127             {
128                 "stroke-width": 2,
129                 "stroke-opacity": 1,
130                 stroke: 'rgb(100, 100, 100)',
131                 translate: {
132                     x: 0.6,
133                     y: 1
134                 }
135             }]
136         });
137         me.group = surface.getGroup(me.seriesId);
138         if (shadow) {
139             for (i = 0, l = me.shadowAttributes.length; i < l; i++) {
140                 me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i));
141             }
142         }
143         surface.customAttributes.segment = function(opt) {
144             return me.getSegment(opt);
145         };
146     },
147     
148     //@private updates some onbefore render parameters.
149     initialize: function() {
150         var me = this,
151             store = me.chart.substore || me.chart.store;
152         //Add yFields to be used in Legend.js
153         me.yField = [];
154         if (me.label.field) {
155             store.each(function(rec) {
156                 me.yField.push(rec.get(me.label.field));
157             });
158         }
159     },
160
161     // @private returns an object with properties for a Slice
162     getSegment: function(opt) {
163         var me = this,
164             rad = me.rad,
165             cos = Math.cos,
166             sin = Math.sin,
167             abs = Math.abs,
168             x = me.centerX,
169             y = me.centerY,
170             x1 = 0, x2 = 0, x3 = 0, x4 = 0,
171             y1 = 0, y2 = 0, y3 = 0, y4 = 0,
172             delta = 1e-2,
173             r = opt.endRho - opt.startRho,
174             startAngle = opt.startAngle,
175             endAngle = opt.endAngle,
176             midAngle = (startAngle + endAngle) / 2 * rad,
177             margin = opt.margin || 0,
178             flag = abs(endAngle - startAngle) > 180,
179             a1 = Math.min(startAngle, endAngle) * rad,
180             a2 = Math.max(startAngle, endAngle) * rad,
181             singleSlice = false;
182
183         x += margin * cos(midAngle);
184         y += margin * sin(midAngle);
185
186         x1 = x + opt.startRho * cos(a1);
187         y1 = y + opt.startRho * sin(a1);
188
189         x2 = x + opt.endRho * cos(a1);
190         y2 = y + opt.endRho * sin(a1);
191
192         x3 = x + opt.startRho * cos(a2);
193         y3 = y + opt.startRho * sin(a2);
194
195         x4 = x + opt.endRho * cos(a2);
196         y4 = y + opt.endRho * sin(a2);
197
198         if (abs(x1 - x3) <= delta && abs(y1 - y3) <= delta) {
199             singleSlice = true;
200         }
201         //Solves mysterious clipping bug with IE
202         if (singleSlice) {
203             return {
204                 path: [
205                 ["M", x1, y1],
206                 ["L", x2, y2],
207                 ["A", opt.endRho, opt.endRho, 0, +flag, 1, x4, y4],
208                 ["Z"]]
209             };
210         } else {
211             return {
212                 path: [
213                 ["M", x1, y1],
214                 ["L", x2, y2],
215                 ["A", opt.endRho, opt.endRho, 0, +flag, 1, x4, y4],
216                 ["L", x3, y3],
217                 ["A", opt.startRho, opt.startRho, 0, +flag, 0, x1, y1],
218                 ["Z"]]
219             };
220         }
221     },
222
223     // @private utility function to calculate the middle point of a pie slice.
224     calcMiddle: function(item) {
225         var me = this,
226             rad = me.rad,
227             slice = item.slice,
228             x = me.centerX,
229             y = me.centerY,
230             startAngle = slice.startAngle,
231             endAngle = slice.endAngle,
232             radius = Math.max(('rho' in slice) ? slice.rho: me.radius, me.label.minMargin),
233             donut = +me.donut,
234             a1 = Math.min(startAngle, endAngle) * rad,
235             a2 = Math.max(startAngle, endAngle) * rad,
236             midAngle = -(a1 + (a2 - a1) / 2),
237             xm = x + (item.endRho + item.startRho) / 2 * Math.cos(midAngle),
238             ym = y - (item.endRho + item.startRho) / 2 * Math.sin(midAngle);
239
240         item.middle = {
241             x: xm,
242             y: ym
243         };
244     },
245
246     /**
247      * Draws the series for the current chart.
248      */
249     drawSeries: function() {
250         var me = this,
251             chart = me.chart,
252             store = chart.substore || chart.store,
253             group = me.group,
254             animate = me.chart.animate,
255             axis = me.chart.axes.get(0),
256             minimum = axis && axis.minimum || me.minimum || 0,
257             maximum = axis && axis.maximum || me.maximum || 0,
258             field = me.angleField || me.field || me.xField,
259             surface = chart.surface,
260             chartBBox = chart.chartBBox,
261             rad = me.rad,
262             donut = +me.donut,
263             values = {},
264             items = [],
265             seriesStyle = me.seriesStyle,
266             seriesLabelStyle = me.seriesLabelStyle,
267             colorArrayStyle = me.colorArrayStyle,
268             colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0,
269             gutterX = chart.maxGutter[0],
270             gutterY = chart.maxGutter[1],
271             cos = Math.cos,
272             sin = Math.sin,
273             rendererAttributes, centerX, centerY, slice, slices, sprite, value,
274             item, ln, record, i, j, startAngle, endAngle, middleAngle, sliceLength, path,
275             p, spriteOptions, bbox, splitAngle, sliceA, sliceB;
276         
277         Ext.apply(seriesStyle, me.style || {});
278
279         me.setBBox();
280         bbox = me.bbox;
281
282         //override theme colors
283         if (me.colorSet) {
284             colorArrayStyle = me.colorSet;
285             colorArrayLength = colorArrayStyle.length;
286         }
287         
288         //if not store or store is empty then there's nothing to draw
289         if (!store || !store.getCount()) {
290             return;
291         }
292         
293         centerX = me.centerX = chartBBox.x + (chartBBox.width / 2);
294         centerY = me.centerY = chartBBox.y + chartBBox.height;
295         me.radius = Math.min(centerX - chartBBox.x, centerY - chartBBox.y);
296         me.slices = slices = [];
297         me.items = items = [];
298         
299         if (!me.value) {
300             record = store.getAt(0);
301             me.value = record.get(field);
302         }
303         
304         value = me.value;
305         if (me.needle) {
306             sliceA = {
307                 series: me,
308                 value: value,
309                 startAngle: -180,
310                 endAngle: 0,
311                 rho: me.radius
312             };
313             splitAngle = -180 * (1 - (value - minimum) / (maximum - minimum));
314             slices.push(sliceA);
315         } else {
316             splitAngle = -180 * (1 - (value - minimum) / (maximum - minimum));
317             sliceA = {
318                 series: me,
319                 value: value,
320                 startAngle: -180,
321                 endAngle: splitAngle,
322                 rho: me.radius
323             };
324             sliceB = {
325                 series: me,
326                 value: me.maximum - value,
327                 startAngle: splitAngle,
328                 endAngle: 0,
329                 rho: me.radius
330             };
331             slices.push(sliceA, sliceB);
332         }
333         
334         //do pie slices after.
335         for (i = 0, ln = slices.length; i < ln; i++) {
336             slice = slices[i];
337             sprite = group.getAt(i);
338             //set pie slice properties
339             rendererAttributes = Ext.apply({
340                 segment: {
341                     startAngle: slice.startAngle,
342                     endAngle: slice.endAngle,
343                     margin: 0,
344                     rho: slice.rho,
345                     startRho: slice.rho * +donut / 100,
346                     endRho: slice.rho
347                 } 
348             }, Ext.apply(seriesStyle, colorArrayStyle && { fill: colorArrayStyle[i % colorArrayLength] } || {}));
349
350             item = Ext.apply({},
351             rendererAttributes.segment, {
352                 slice: slice,
353                 series: me,
354                 storeItem: record,
355                 index: i
356             });
357             items[i] = item;
358             // Create a new sprite if needed (no height)
359             if (!sprite) {
360                 spriteOptions = Ext.apply({
361                     type: "path",
362                     group: group
363                 }, Ext.apply(seriesStyle, colorArrayStyle && { fill: colorArrayStyle[i % colorArrayLength] } || {}));
364                 sprite = surface.add(Ext.apply(spriteOptions, rendererAttributes));
365             }
366             slice.sprite = slice.sprite || [];
367             item.sprite = sprite;
368             slice.sprite.push(sprite);
369             if (animate) {
370                 rendererAttributes = me.renderer(sprite, record, rendererAttributes, i, store);
371                 sprite._to = rendererAttributes;
372                 me.onAnimate(sprite, {
373                     to: rendererAttributes
374                 });
375             } else {
376                 rendererAttributes = me.renderer(sprite, record, Ext.apply(rendererAttributes, {
377                     hidden: false
378                 }), i, store);
379                 sprite.setAttributes(rendererAttributes, true);
380             }
381         }
382         
383         if (me.needle) {
384             splitAngle = splitAngle * Math.PI / 180;
385             
386             if (!me.needleSprite) {
387                 me.needleSprite = me.chart.surface.add({
388                     type: 'path',
389                     path: ['M', centerX + (me.radius * +donut / 100) * cos(splitAngle),
390                                 centerY + -Math.abs((me.radius * +donut / 100) * sin(splitAngle)),
391                            'L', centerX + me.radius * cos(splitAngle),
392                                 centerY + -Math.abs(me.radius * sin(splitAngle))],
393                     'stroke-width': 4,
394                     'stroke': '#222'
395                 });
396             } else {
397                 if (animate) {
398                     me.onAnimate(me.needleSprite, {
399                         to: {
400                         path: ['M', centerX + (me.radius * +donut / 100) * cos(splitAngle),
401                                     centerY + -Math.abs((me.radius * +donut / 100) * sin(splitAngle)),
402                                'L', centerX + me.radius * cos(splitAngle),
403                                     centerY + -Math.abs(me.radius * sin(splitAngle))]
404                         }
405                     });
406                 } else {
407                     me.needleSprite.setAttributes({
408                         type: 'path',
409                         path: ['M', centerX + (me.radius * +donut / 100) * cos(splitAngle),
410                                     centerY + -Math.abs((me.radius * +donut / 100) * sin(splitAngle)),
411                                'L', centerX + me.radius * cos(splitAngle),
412                                     centerY + -Math.abs(me.radius * sin(splitAngle))]
413                     });
414                 }
415             }
416             me.needleSprite.setAttributes({
417                 hidden: false    
418             }, true);
419         }
420         
421         delete me.value;
422     },
423     
424     /**
425      * Sets the Gauge chart to the current specified value.
426     */
427     setValue: function (value) {
428         this.value = value;
429         this.drawSeries();
430     },
431
432     // @private callback for when creating a label sprite.
433     onCreateLabel: function(storeItem, item, i, display) {},
434
435     // @private callback for when placing a label sprite.
436     onPlaceLabel: function(label, storeItem, item, i, display, animate, index) {},
437
438     // @private callback for when placing a callout.
439     onPlaceCallout: function() {},
440
441     // @private handles sprite animation for the series.
442     onAnimate: function(sprite, attr) {
443         sprite.show();
444         return this.callParent(arguments);
445     },
446
447     isItemInPoint: function(x, y, item, i) {
448         return false;
449     },
450     
451     // @private shows all elements in the series.
452     showAll: function() {
453         if (!isNaN(this._index)) {
454             this.__excludes[this._index] = false;
455             this.drawSeries();
456         }
457     },
458     
459     /**
460      * Returns the color of the series (to be displayed as color for the series legend item).
461      * @param item {Object} Info about the item; same format as returned by #getItemForPoint
462      */
463     getLegendColor: function(index) {
464         var me = this;
465         return me.colorArrayStyle[index % me.colorArrayStyle.length];
466     }
467 });
468
469