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