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