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