Upgrade to ExtJS 4.0.0 - Released 04/26/2011
[extjs.git] / src / chart / series / Radar.js
diff --git a/src/chart/series/Radar.js b/src/chart/series/Radar.js
new file mode 100644 (file)
index 0000000..0f5b155
--- /dev/null
@@ -0,0 +1,420 @@
+/**
+ * @class Ext.chart.series.Radar
+ * @extends Ext.chart.series.Series
+ * 
+ * Creates a Radar Chart. A Radar Chart is a useful visualization technique for comparing different quantitative values for 
+ * a constrained number of categories.
+ * As with all other series, the Radar series must be appended in the *series* Chart array configuration. See the Chart 
+ * documentation for more information. A typical configuration object for the radar series could be:
+ * 
+ {@img Ext.chart.series.Radar/Ext.chart.series.Radar.png Ext.chart.series.Radar chart series}  
+  <pre><code>
+    var store = Ext.create('Ext.data.JsonStore', {
+        fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
+        data: [
+            {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
+            {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
+            {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
+            {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
+            {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}                                                
+        ]
+    });
+    
+    Ext.create('Ext.chart.Chart', {
+        renderTo: Ext.getBody(),
+        width: 500,
+        height: 300,
+        animate: true,
+        theme:'Category2',
+        store: store,
+        axes: [{
+            type: 'Radial',
+            position: 'radial',
+            label: {
+                display: true
+            }
+        }],
+        series: [{
+            type: 'radar',
+            xField: 'name',
+            yField: 'data3',
+            showInLegend: true,
+            showMarkers: true,
+            markerConfig: {
+                radius: 5,
+                size: 5           
+            },
+            style: {
+                'stroke-width': 2,
+                fill: 'none'
+            }
+        },{
+            type: 'radar',
+            xField: 'name',
+            yField: 'data2',
+            showMarkers: true,
+            showInLegend: true,
+            markerConfig: {
+                radius: 5,
+                size: 5
+            },
+            style: {
+                'stroke-width': 2,
+                fill: 'none'
+            }
+        },{
+            type: 'radar',
+            xField: 'name',
+            yField: 'data5',
+            showMarkers: true,
+            showInLegend: true,
+            markerConfig: {
+                radius: 5,
+                size: 5
+            },
+            style: {
+                'stroke-width': 2,
+                fill: 'none'
+            }
+        }]    
+    });
+   </code></pre>
+ * 
+ * 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,
+ * `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 
+ * the markerConfig object. Finally we override some theme styling properties by adding properties to the `style` object.
+ * 
+ * @xtype radar
+ * 
+ */
+Ext.define('Ext.chart.series.Radar', {
+
+    /* Begin Definitions */
+
+    extend: 'Ext.chart.series.Series',
+
+    requires: ['Ext.chart.Shape', 'Ext.fx.Anim'],
+
+    /* End Definitions */
+
+    type: "radar",
+    alias: 'series.radar',
+
+    
+    rad: Math.PI / 180,
+
+    showInLegend: false,
+
+    /**
+     * @cfg {Object} style
+     * An object containing styles for overriding series styles from Theming.
+     */
+    style: {},
+    
+    constructor: function(config) {
+        this.callParent(arguments);
+        var me = this,
+            surface = me.chart.surface, i, l;
+        me.group = surface.getGroup(me.seriesId);
+        if (me.showMarkers) {
+            me.markerGroup = surface.getGroup(me.seriesId + '-markers');
+        }
+    },
+
+    /**
+     * Draws the series for the current chart.
+     */
+    drawSeries: function() {
+        var me = this,
+            store = me.chart.substore || me.chart.store,
+            group = me.group,
+            sprite,
+            chart = me.chart,
+            animate = chart.animate,
+            field = me.field || me.yField,
+            surface = chart.surface,
+            chartBBox = chart.chartBBox,
+            rendererAttributes,
+            centerX, centerY,
+            items,
+            radius,
+            maxValue = 0,
+            fields = [],
+            max = Math.max,
+            cos = Math.cos,
+            sin = Math.sin,
+            pi2 = Math.PI * 2,
+            l = store.getCount(),
+            startPath, path, x, y, rho,
+            i, nfields,
+            seriesStyle = me.seriesStyle,
+            seriesLabelStyle = me.seriesLabelStyle,
+            first = chart.resizing || !me.radar,
+            axis = chart.axes && chart.axes.get(0),
+            aggregate = !(axis && axis.maximum);
+        
+        me.setBBox();
+
+        maxValue = aggregate? 0 : (axis.maximum || 0);
+        
+        Ext.apply(seriesStyle, me.style || {});
+        
+        //if the store is empty then there's nothing to draw
+        if (!store || !store.getCount()) {
+            return;
+        }
+        
+        me.unHighlightItem();
+        me.cleanHighlights();
+
+        centerX = me.centerX = chartBBox.x + (chartBBox.width / 2);
+        centerY = me.centerY = chartBBox.y + (chartBBox.height / 2);
+        me.radius = radius = Math.min(chartBBox.width, chartBBox.height) /2;
+        me.items = items = [];
+
+        if (aggregate) {
+            //get all renderer fields
+            chart.series.each(function(series) {
+                fields.push(series.yField);
+            });
+            //get maxValue to interpolate
+            store.each(function(record, i) {
+                for (i = 0, nfields = fields.length; i < nfields; i++) {
+                    maxValue = max(+record.get(fields[i]), maxValue);
+                }
+            });
+        }
+        //ensure non-zero value.
+        maxValue = maxValue || 1;
+        //create path and items
+        startPath = []; path = [];
+        store.each(function(record, i) {
+            rho = radius * record.get(field) / maxValue;
+            x = rho * cos(i / l * pi2);
+            y = rho * sin(i / l * pi2);
+            if (i == 0) {
+                path.push('M', x + centerX, y + centerY);
+                startPath.push('M', 0.01 * x + centerX, 0.01 * y + centerY);
+            } else {
+                path.push('L', x + centerX, y + centerY);
+                startPath.push('L', 0.01 * x + centerX, 0.01 * y + centerY);
+            }
+            items.push({
+                sprite: false, //TODO(nico): add markers
+                point: [centerX + x, centerY + y],
+                series: me
+            });
+        });
+        path.push('Z');
+        //create path sprite
+        if (!me.radar) {
+            me.radar = surface.add(Ext.apply({
+                type: 'path',
+                group: group,
+                path: startPath
+            }, seriesStyle || {}));
+        }
+        //reset on resizing
+        if (chart.resizing) {
+            me.radar.setAttributes({
+                path: startPath
+            }, true);
+        }
+        //render/animate
+        if (chart.animate) {
+            me.onAnimate(me.radar, {
+                to: Ext.apply({
+                    path: path
+                }, seriesStyle || {})
+            });
+        } else {
+            me.radar.setAttributes(Ext.apply({
+                path: path
+            }, seriesStyle || {}), true);
+        }
+        //render markers, labels and callouts
+        if (me.showMarkers) {
+            me.drawMarkers();
+        }
+        me.renderLabels();
+        me.renderCallouts();
+    },
+    
+    // @private draws the markers for the lines (if any).
+    drawMarkers: function() {
+        var me = this,
+            chart = me.chart,
+            surface = chart.surface,
+            markerStyle = Ext.apply({}, me.markerStyle || {}),
+            endMarkerStyle = Ext.apply(markerStyle, me.markerConfig),
+            items = me.items, 
+            type = endMarkerStyle.type,
+            markerGroup = me.markerGroup,
+            centerX = me.centerX,
+            centerY = me.centerY,
+            item, i, l, marker;
+        
+        delete endMarkerStyle.type;
+        
+        for (i = 0, l = items.length; i < l; i++) {
+            item = items[i];
+            marker = markerGroup.getAt(i);
+            if (!marker) {
+                marker = Ext.chart.Shape[type](surface, Ext.apply({
+                    group: markerGroup,
+                    x: 0,
+                    y: 0,
+                    translate: {
+                        x: centerX,
+                        y: centerY
+                    }
+                }, endMarkerStyle));
+            }
+            else {
+                marker.show();
+            }
+            if (chart.resizing) {
+                marker.setAttributes({
+                    x: 0,
+                    y: 0,
+                    translate: {
+                        x: centerX,
+                        y: centerY
+                    }
+                }, true);
+            }
+            marker._to = {
+                translate: {
+                    x: item.point[0],
+                    y: item.point[1]
+                }
+            };
+            //render/animate
+            if (chart.animate) {
+                me.onAnimate(marker, {
+                    to: marker._to
+                });
+            }
+            else {
+                marker.setAttributes(Ext.apply(marker._to, endMarkerStyle || {}), true);
+            }
+        }
+    },
+    
+    isItemInPoint: function(x, y, item) {
+        var point,
+            tolerance = 10,
+            abs = Math.abs;
+        point = item.point;
+        return (abs(point[0] - x) <= tolerance &&
+                abs(point[1] - y) <= tolerance);
+    },
+
+    // @private callback for when creating a label sprite.
+    onCreateLabel: function(storeItem, item, i, display) {
+        var me = this,
+            group = me.labelsGroup,
+            config = me.label,
+            centerX = me.centerX,
+            centerY = me.centerY,
+            point = item.point,
+            endLabelStyle = Ext.apply(me.seriesLabelStyle || {}, config);
+        
+        return me.chart.surface.add(Ext.apply({
+            'type': 'text',
+            'text-anchor': 'middle',
+            'group': group,
+            'x': centerX,
+            'y': centerY
+        }, config || {}));
+    },
+
+    // @private callback for when placing a label sprite.
+    onPlaceLabel: function(label, storeItem, item, i, display, animate) {
+        var me = this,
+            chart = me.chart,
+            resizing = chart.resizing,
+            config = me.label,
+            format = config.renderer,
+            field = config.field,
+            centerX = me.centerX,
+            centerY = me.centerY,
+            opt = {
+                x: item.point[0],
+                y: item.point[1]
+            },
+            x = opt.x - centerX,
+            y = opt.y - centerY;
+
+        label.setAttributes({
+            text: format(storeItem.get(field)),
+            hidden: true
+        },
+        true);
+        
+        if (resizing) {
+            label.setAttributes({
+                x: centerX,
+                y: centerY
+            }, true);
+        }
+        
+        if (animate) {
+            label.show(true);
+            me.onAnimate(label, {
+                to: opt
+            });
+        } else {
+            label.setAttributes(opt, true);
+            label.show(true);
+        }
+    },
+
+    // @private for toggling (show/hide) series. 
+    toggleAll: function(show) {
+        var me = this,
+            i, ln, shadow, shadows;
+        if (!show) {
+            Ext.chart.series.Radar.superclass.hideAll.call(me);
+        }
+        else {
+            Ext.chart.series.Radar.superclass.showAll.call(me);
+        }
+        if (me.radar) {
+            me.radar.setAttributes({
+                hidden: !show
+            }, true);
+            //hide shadows too
+            if (me.radar.shadows) {
+                for (i = 0, shadows = me.radar.shadows, ln = shadows.length; i < ln; i++) {
+                    shadow = shadows[i];
+                    shadow.setAttributes({
+                        hidden: !show
+                    }, true);
+                }
+            }
+        }
+    },
+    
+    // @private hide all elements in the series.
+    hideAll: function() {
+        this.toggleAll(false);
+        this.hideMarkers(0);
+    },
+    
+    // @private show all elements in the series.
+    showAll: function() {
+        this.toggleAll(true);
+    },
+    
+    // @private hide all markers that belong to `markerGroup`
+    hideMarkers: function(index) {
+        var me = this,
+            count = me.markerGroup && me.markerGroup.getCount() || 0,
+            i = index || 0;
+        for (; i < count; i++) {
+            me.markerGroup.getAt(i).hide(true);
+        }
+    }
+});
+