Upgrade to ExtJS 4.0.2 - Released 06/09/2011
[extjs.git] / src / chart / series / Radar.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.Radar
17  * @extends Ext.chart.series.Series
18  * 
19  * Creates a Radar Chart. A Radar Chart is a useful visualization technique for comparing different quantitative values for 
20  * a constrained number of categories.
21  * As with all other series, the Radar series must be appended in the *series* Chart array configuration. See the Chart 
22  * documentation for more information. A typical configuration object for the radar series could be:
23  * 
24  * {@img Ext.chart.series.Radar/Ext.chart.series.Radar.png Ext.chart.series.Radar chart series}  
25  *
26  *     var store = Ext.create('Ext.data.JsonStore', {
27  *         fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
28  *         data: [
29  *             {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
30  *             {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
31  *             {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
32  *             {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
33  *             {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}                                                
34  *         ]
35  *     });
36  *     
37  *     Ext.create('Ext.chart.Chart', {
38  *         renderTo: Ext.getBody(),
39  *         width: 500,
40  *         height: 300,
41  *         animate: true,
42  *         theme:'Category2',
43  *         store: store,
44  *         axes: [{
45  *             type: 'Radial',
46  *             position: 'radial',
47  *             label: {
48  *                 display: true
49  *             }
50  *         }],
51  *         series: [{
52  *             type: 'radar',
53  *             xField: 'name',
54  *             yField: 'data3',
55  *             showInLegend: true,
56  *             showMarkers: true,
57  *             markerConfig: {
58  *                 radius: 5,
59  *                 size: 5           
60  *             },
61  *             style: {
62  *                 'stroke-width': 2,
63  *                 fill: 'none'
64  *             }
65  *         },{
66  *             type: 'radar',
67  *             xField: 'name',
68  *             yField: 'data2',
69  *             showMarkers: true,
70  *             showInLegend: true,
71  *             markerConfig: {
72  *                 radius: 5,
73  *                 size: 5
74  *             },
75  *             style: {
76  *                 'stroke-width': 2,
77  *                 fill: 'none'
78  *             }
79  *         },{
80  *             type: 'radar',
81  *             xField: 'name',
82  *             yField: 'data5',
83  *             showMarkers: true,
84  *             showInLegend: true,
85  *             markerConfig: {
86  *                 radius: 5,
87  *                 size: 5
88  *             },
89  *             style: {
90  *                 'stroke-width': 2,
91  *                 fill: 'none'
92  *             }
93  *         }]    
94  *     });
95  * 
96  * In this configuration we add three series to the chart. Each of these series is bound to the same categories field, `name` but bound to different properties for each category,
97  * `data1`, `data2` and `data3` respectively. All series display markers by having `showMarkers` enabled. The configuration for the markers of each series can be set by adding properties onto 
98  * the markerConfig object. Finally we override some theme styling properties by adding properties to the `style` object.
99  * 
100  * @xtype radar
101  */
102 Ext.define('Ext.chart.series.Radar', {
103
104     /* Begin Definitions */
105
106     extend: 'Ext.chart.series.Series',
107
108     requires: ['Ext.chart.Shape', 'Ext.fx.Anim'],
109
110     /* End Definitions */
111
112     type: "radar",
113     alias: 'series.radar',
114
115     
116     rad: Math.PI / 180,
117
118     showInLegend: false,
119
120     /**
121      * @cfg {Object} style
122      * An object containing styles for overriding series styles from Theming.
123      */
124     style: {},
125     
126     constructor: function(config) {
127         this.callParent(arguments);
128         var me = this,
129             surface = me.chart.surface, i, l;
130         me.group = surface.getGroup(me.seriesId);
131         if (me.showMarkers) {
132             me.markerGroup = surface.getGroup(me.seriesId + '-markers');
133         }
134     },
135
136     /**
137      * Draws the series for the current chart.
138      */
139     drawSeries: function() {
140         var me = this,
141             store = me.chart.substore || me.chart.store,
142             group = me.group,
143             sprite,
144             chart = me.chart,
145             animate = chart.animate,
146             field = me.field || me.yField,
147             surface = chart.surface,
148             chartBBox = chart.chartBBox,
149             rendererAttributes,
150             centerX, centerY,
151             items,
152             radius,
153             maxValue = 0,
154             fields = [],
155             max = Math.max,
156             cos = Math.cos,
157             sin = Math.sin,
158             pi2 = Math.PI * 2,
159             l = store.getCount(),
160             startPath, path, x, y, rho,
161             i, nfields,
162             seriesStyle = me.seriesStyle,
163             seriesLabelStyle = me.seriesLabelStyle,
164             first = chart.resizing || !me.radar,
165             axis = chart.axes && chart.axes.get(0),
166             aggregate = !(axis && axis.maximum);
167         
168         me.setBBox();
169
170         maxValue = aggregate? 0 : (axis.maximum || 0);
171         
172         Ext.apply(seriesStyle, me.style || {});
173         
174         //if the store is empty then there's nothing to draw
175         if (!store || !store.getCount()) {
176             return;
177         }
178         
179         me.unHighlightItem();
180         me.cleanHighlights();
181
182         centerX = me.centerX = chartBBox.x + (chartBBox.width / 2);
183         centerY = me.centerY = chartBBox.y + (chartBBox.height / 2);
184         me.radius = radius = Math.min(chartBBox.width, chartBBox.height) /2;
185         me.items = items = [];
186
187         if (aggregate) {
188             //get all renderer fields
189             chart.series.each(function(series) {
190                 fields.push(series.yField);
191             });
192             //get maxValue to interpolate
193             store.each(function(record, i) {
194                 for (i = 0, nfields = fields.length; i < nfields; i++) {
195                     maxValue = max(+record.get(fields[i]), maxValue);
196                 }
197             });
198         }
199         //ensure non-zero value.
200         maxValue = maxValue || 1;
201         //create path and items
202         startPath = []; path = [];
203         store.each(function(record, i) {
204             rho = radius * record.get(field) / maxValue;
205             x = rho * cos(i / l * pi2);
206             y = rho * sin(i / l * pi2);
207             if (i == 0) {
208                 path.push('M', x + centerX, y + centerY);
209                 startPath.push('M', 0.01 * x + centerX, 0.01 * y + centerY);
210             } else {
211                 path.push('L', x + centerX, y + centerY);
212                 startPath.push('L', 0.01 * x + centerX, 0.01 * y + centerY);
213             }
214             items.push({
215                 sprite: false, //TODO(nico): add markers
216                 point: [centerX + x, centerY + y],
217                 series: me
218             });
219         });
220         path.push('Z');
221         //create path sprite
222         if (!me.radar) {
223             me.radar = surface.add(Ext.apply({
224                 type: 'path',
225                 group: group,
226                 path: startPath
227             }, seriesStyle || {}));
228         }
229         //reset on resizing
230         if (chart.resizing) {
231             me.radar.setAttributes({
232                 path: startPath
233             }, true);
234         }
235         //render/animate
236         if (chart.animate) {
237             me.onAnimate(me.radar, {
238                 to: Ext.apply({
239                     path: path
240                 }, seriesStyle || {})
241             });
242         } else {
243             me.radar.setAttributes(Ext.apply({
244                 path: path
245             }, seriesStyle || {}), true);
246         }
247         //render markers, labels and callouts
248         if (me.showMarkers) {
249             me.drawMarkers();
250         }
251         me.renderLabels();
252         me.renderCallouts();
253     },
254     
255     // @private draws the markers for the lines (if any).
256     drawMarkers: function() {
257         var me = this,
258             chart = me.chart,
259             surface = chart.surface,
260             markerStyle = Ext.apply({}, me.markerStyle || {}),
261             endMarkerStyle = Ext.apply(markerStyle, me.markerConfig),
262             items = me.items, 
263             type = endMarkerStyle.type,
264             markerGroup = me.markerGroup,
265             centerX = me.centerX,
266             centerY = me.centerY,
267             item, i, l, marker;
268         
269         delete endMarkerStyle.type;
270         
271         for (i = 0, l = items.length; i < l; i++) {
272             item = items[i];
273             marker = markerGroup.getAt(i);
274             if (!marker) {
275                 marker = Ext.chart.Shape[type](surface, Ext.apply({
276                     group: markerGroup,
277                     x: 0,
278                     y: 0,
279                     translate: {
280                         x: centerX,
281                         y: centerY
282                     }
283                 }, endMarkerStyle));
284             }
285             else {
286                 marker.show();
287             }
288             if (chart.resizing) {
289                 marker.setAttributes({
290                     x: 0,
291                     y: 0,
292                     translate: {
293                         x: centerX,
294                         y: centerY
295                     }
296                 }, true);
297             }
298             marker._to = {
299                 translate: {
300                     x: item.point[0],
301                     y: item.point[1]
302                 }
303             };
304             //render/animate
305             if (chart.animate) {
306                 me.onAnimate(marker, {
307                     to: marker._to
308                 });
309             }
310             else {
311                 marker.setAttributes(Ext.apply(marker._to, endMarkerStyle || {}), true);
312             }
313         }
314     },
315     
316     isItemInPoint: function(x, y, item) {
317         var point,
318             tolerance = 10,
319             abs = Math.abs;
320         point = item.point;
321         return (abs(point[0] - x) <= tolerance &&
322                 abs(point[1] - y) <= tolerance);
323     },
324
325     // @private callback for when creating a label sprite.
326     onCreateLabel: function(storeItem, item, i, display) {
327         var me = this,
328             group = me.labelsGroup,
329             config = me.label,
330             centerX = me.centerX,
331             centerY = me.centerY,
332             point = item.point,
333             endLabelStyle = Ext.apply(me.seriesLabelStyle || {}, config);
334         
335         return me.chart.surface.add(Ext.apply({
336             'type': 'text',
337             'text-anchor': 'middle',
338             'group': group,
339             'x': centerX,
340             'y': centerY
341         }, config || {}));
342     },
343
344     // @private callback for when placing a label sprite.
345     onPlaceLabel: function(label, storeItem, item, i, display, animate) {
346         var me = this,
347             chart = me.chart,
348             resizing = chart.resizing,
349             config = me.label,
350             format = config.renderer,
351             field = config.field,
352             centerX = me.centerX,
353             centerY = me.centerY,
354             opt = {
355                 x: item.point[0],
356                 y: item.point[1]
357             },
358             x = opt.x - centerX,
359             y = opt.y - centerY;
360
361         label.setAttributes({
362             text: format(storeItem.get(field)),
363             hidden: true
364         },
365         true);
366         
367         if (resizing) {
368             label.setAttributes({
369                 x: centerX,
370                 y: centerY
371             }, true);
372         }
373         
374         if (animate) {
375             label.show(true);
376             me.onAnimate(label, {
377                 to: opt
378             });
379         } else {
380             label.setAttributes(opt, true);
381             label.show(true);
382         }
383     },
384
385     // @private for toggling (show/hide) series. 
386     toggleAll: function(show) {
387         var me = this,
388             i, ln, shadow, shadows;
389         if (!show) {
390             Ext.chart.series.Radar.superclass.hideAll.call(me);
391         }
392         else {
393             Ext.chart.series.Radar.superclass.showAll.call(me);
394         }
395         if (me.radar) {
396             me.radar.setAttributes({
397                 hidden: !show
398             }, true);
399             //hide shadows too
400             if (me.radar.shadows) {
401                 for (i = 0, shadows = me.radar.shadows, ln = shadows.length; i < ln; i++) {
402                     shadow = shadows[i];
403                     shadow.setAttributes({
404                         hidden: !show
405                     }, true);
406                 }
407             }
408         }
409     },
410     
411     // @private hide all elements in the series.
412     hideAll: function() {
413         this.toggleAll(false);
414         this.hideMarkers(0);
415     },
416     
417     // @private show all elements in the series.
418     showAll: function() {
419         this.toggleAll(true);
420     },
421     
422     // @private hide all markers that belong to `markerGroup`
423     hideMarkers: function(index) {
424         var me = this,
425             count = me.markerGroup && me.markerGroup.getCount() || 0,
426             i = index || 0;
427         for (; i < count; i++) {
428             me.markerGroup.getAt(i).hide(true);
429         }
430     }
431 });
432
433