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