X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/0494b8d9b9bb03ab6c22b34dae81261e3cd7e3e6..7a654f8d43fdb43d78b63d90528bed6e86b608cc:/src/chart/Chart.js diff --git a/src/chart/Chart.js b/src/chart/Chart.js new file mode 100644 index 00000000..1c15d2fb --- /dev/null +++ b/src/chart/Chart.js @@ -0,0 +1,756 @@ +/** + * @class Ext.chart.Chart + * @extends Ext.draw.Component + * + * The Ext.chart package provides the capability to visualize data. + * Each chart binds directly to an Ext.data.Store enabling automatic updates of the chart. + * A chart configuration object has some overall styling options as well as an array of axes + * and series. A chart instance example could look like: + * +

+    Ext.create('Ext.chart.Chart', {
+        renderTo: Ext.getBody(),
+        width: 800,
+        height: 600,
+        animate: true,
+        store: store1,
+        shadow: true,
+        theme: 'Category1',
+        legend: {
+            position: 'right'
+        },
+        axes: [ ...some axes options... ],
+        series: [ ...some series options... ]
+    });
+  
+ * + * In this example we set the `width` and `height` of the chart, we decide whether our series are + * animated or not and we select a store to be bound to the chart. We also turn on shadows for all series, + * select a color theme `Category1` for coloring the series, set the legend to the right part of the chart and + * then tell the chart to render itself in the body element of the document. For more information about the axes and + * series configurations please check the documentation of each series (Line, Bar, Pie, etc). + * + * @xtype chart + */ + +Ext.define('Ext.chart.Chart', { + + /* Begin Definitions */ + + alias: 'widget.chart', + + extend: 'Ext.draw.Component', + + mixins: { + themeManager: 'Ext.chart.theme.Theme', + mask: 'Ext.chart.Mask', + navigation: 'Ext.chart.Navigation' + }, + + requires: [ + 'Ext.util.MixedCollection', + 'Ext.data.StoreManager', + 'Ext.chart.Legend', + 'Ext.util.DelayedTask' + ], + + /* End Definitions */ + + // @private + viewBox: false, + + /** + * @cfg {String} theme (optional) The name of the theme to be used. A theme defines the colors and + * other visual displays of tick marks on axis, text, title text, line colors, marker colors and styles, etc. + * Possible theme values are 'Base', 'Green', 'Sky', 'Red', 'Purple', 'Blue', 'Yellow' and also six category themes + * 'Category1' to 'Category6'. Default value is 'Base'. + */ + + /** + * @cfg {Boolean/Object} animate (optional) true for the default animation (easing: 'ease' and duration: 500) + * or a standard animation config object to be used for default chart animations. Defaults to false. + */ + animate: false, + + /** + * @cfg {Boolean/Object} legend (optional) true for the default legend display or a legend config object. Defaults to false. + */ + legend: false, + + /** + * @cfg {integer} insetPadding (optional) Set the amount of inset padding in pixels for the chart. Defaults to 10. + */ + insetPadding: 10, + + /** + * @cfg {Array} enginePriority + * Defines the priority order for which Surface implementation to use. The first + * one supported by the current environment will be used. + */ + enginePriority: ['Svg', 'Vml'], + + /** + * @cfg {Object|Boolean} background (optional) Set the chart background. This can be a gradient object, image, or color. + * Defaults to false for no background. + * + * For example, if `background` were to be a color we could set the object as + * +

+        background: {
+            //color string
+            fill: '#ccc'
+        }
+     
+ + You can specify an image by using: + +

+        background: {
+            image: 'http://path.to.image/'
+        }
+     
+ + Also you can specify a gradient by using the gradient object syntax: + +

+        background: {
+            gradient: {
+                id: 'gradientId',
+                angle: 45,
+                stops: {
+                    0: {
+                        color: '#555'
+                    }
+                    100: {
+                        color: '#ddd'
+                    }
+                }
+            }
+        }
+     
+ */ + background: false, + + /** + * @cfg {Array} gradients (optional) Define a set of gradients that can be used as `fill` property in sprites. + * The gradients array is an array of objects with the following properties: + * + * + * + + For example: + +

+        gradients: [{
+            id: 'gradientId',
+            angle: 45,
+            stops: {
+                0: {
+                    color: '#555'
+                },
+                100: {
+                    color: '#ddd'
+                }
+            }
+        },  {
+            id: 'gradientId2',
+            angle: 0,
+            stops: {
+                0: {
+                    color: '#590'
+                },
+                20: {
+                    color: '#599'
+                },
+                100: {
+                    color: '#ddd'
+                }
+            }
+        }]
+     
+ + Then the sprites can use `gradientId` and `gradientId2` by setting the fill attributes to those ids, for example: + +

+        sprite.setAttributes({
+            fill: 'url(#gradientId)'
+        }, true);
+     
+ + */ + + + constructor: function(config) { + var me = this, + defaultAnim; + me.initTheme(config.theme || me.theme); + if (me.gradients) { + Ext.apply(config, { gradients: me.gradients }); + } + if (me.background) { + Ext.apply(config, { background: me.background }); + } + if (config.animate) { + defaultAnim = { + easing: 'ease', + duration: 500 + }; + if (Ext.isObject(config.animate)) { + config.animate = Ext.applyIf(config.animate, defaultAnim); + } + else { + config.animate = defaultAnim; + } + } + me.mixins.mask.constructor.call(me, config); + me.mixins.navigation.constructor.call(me, config); + me.callParent([config]); + }, + + initComponent: function() { + var me = this, + axes, + series; + me.callParent(); + me.addEvents( + 'itemmousedown', + 'itemmouseup', + 'itemmouseover', + 'itemmouseout', + 'itemclick', + 'itemdoubleclick', + 'itemdragstart', + 'itemdrag', + 'itemdragend', + /** + * @event beforerefresh + * Fires before a refresh to the chart data is called. If the beforerefresh handler returns + * false the {@link #refresh} action will be cancelled. + * @param {Chart} this + */ + 'beforerefresh', + /** + * @event refresh + * Fires after the chart data has been refreshed. + * @param {Chart} this + */ + 'refresh' + ); + Ext.applyIf(me, { + zoom: { + width: 1, + height: 1, + x: 0, + y: 0 + } + }); + me.maxGutter = [0, 0]; + me.store = Ext.data.StoreManager.lookup(me.store); + axes = me.axes; + me.axes = Ext.create('Ext.util.MixedCollection', false, function(a) { return a.position; }); + if (axes) { + me.axes.addAll(axes); + } + series = me.series; + me.series = Ext.create('Ext.util.MixedCollection', false, function(a) { return a.seriesId || (a.seriesId = Ext.id(null, 'ext-chart-series-')); }); + if (series) { + me.series.addAll(series); + } + if (me.legend !== false) { + me.legend = Ext.create('Ext.chart.Legend', Ext.applyIf({chart:me}, me.legend)); + } + + me.on({ + mousemove: me.onMouseMove, + mouseleave: me.onMouseLeave, + mousedown: me.onMouseDown, + mouseup: me.onMouseUp, + scope: me + }); + }, + + // @private overrides the component method to set the correct dimensions to the chart. + afterComponentLayout: function(width, height) { + var me = this; + if (Ext.isNumber(width) && Ext.isNumber(height)) { + me.curWidth = width; + me.curHeight = height; + me.redraw(true); + } + this.callParent(arguments); + }, + + /** + * Redraw the chart. If animations are set this will animate the chart too. + * @cfg {boolean} resize Optional flag which changes the default origin points of the chart for animations. + */ + redraw: function(resize) { + var me = this, + chartBBox = me.chartBBox = { + x: 0, + y: 0, + height: me.curHeight, + width: me.curWidth + }, + legend = me.legend; + me.surface.setSize(chartBBox.width, chartBBox.height); + // Instantiate Series and Axes + me.series.each(me.initializeSeries, me); + me.axes.each(me.initializeAxis, me); + //process all views (aggregated data etc) on stores + //before rendering. + me.axes.each(function(axis) { + axis.processView(); + }); + me.axes.each(function(axis) { + axis.drawAxis(true); + }); + + // Create legend if not already created + if (legend !== false) { + legend.create(); + } + + // Place axes properly, including influence from each other + me.alignAxes(); + + // Reposition legend based on new axis alignment + if (me.legend !== false) { + legend.updatePosition(); + } + + // Find the max gutter + me.getMaxGutter(); + + // Draw axes and series + me.resizing = !!resize; + + me.axes.each(me.drawAxis, me); + me.series.each(me.drawCharts, me); + me.resizing = false; + }, + + // @private set the store after rendering the chart. + afterRender: function() { + var ref, + me = this; + this.callParent(); + + if (me.categoryNames) { + me.setCategoryNames(me.categoryNames); + } + + if (me.tipRenderer) { + ref = me.getFunctionRef(me.tipRenderer); + me.setTipRenderer(ref.fn, ref.scope); + } + me.bindStore(me.store, true); + me.refresh(); + }, + + // @private get x and y position of the mouse cursor. + getEventXY: function(e) { + var me = this, + box = this.surface.getRegion(), + pageXY = e.getXY(), + x = pageXY[0] - box.left, + y = pageXY[1] - box.top; + return [x, y]; + }, + + // @private wrap the mouse down position to delegate the event to the series. + onClick: function(e) { + var me = this, + position = me.getEventXY(e), + item; + + // Ask each series if it has an item corresponding to (not necessarily exactly + // on top of) the current mouse coords. Fire itemclick event. + me.series.each(function(series) { + if (Ext.draw.Draw.withinBox(position[0], position[1], series.bbox)) { + if (series.getItemForPoint) { + item = series.getItemForPoint(position[0], position[1]); + if (item) { + series.fireEvent('itemclick', item); + } + } + } + }, me); + }, + + // @private wrap the mouse down position to delegate the event to the series. + onMouseDown: function(e) { + var me = this, + position = me.getEventXY(e), + item; + + if (me.mask) { + me.mixins.mask.onMouseDown.call(me, e); + } + // Ask each series if it has an item corresponding to (not necessarily exactly + // on top of) the current mouse coords. Fire mousedown event. + me.series.each(function(series) { + if (Ext.draw.Draw.withinBox(position[0], position[1], series.bbox)) { + if (series.getItemForPoint) { + item = series.getItemForPoint(position[0], position[1]); + if (item) { + series.fireEvent('itemmousedown', item); + } + } + } + }, me); + }, + + // @private wrap the mouse up event to delegate it to the series. + onMouseUp: function(e) { + var me = this, + position = me.getEventXY(e), + item; + + if (me.mask) { + me.mixins.mask.onMouseUp.call(me, e); + } + // Ask each series if it has an item corresponding to (not necessarily exactly + // on top of) the current mouse coords. Fire mousedown event. + me.series.each(function(series) { + if (Ext.draw.Draw.withinBox(position[0], position[1], series.bbox)) { + if (series.getItemForPoint) { + item = series.getItemForPoint(position[0], position[1]); + if (item) { + series.fireEvent('itemmouseup', item); + } + } + } + }, me); + }, + + // @private wrap the mouse move event so it can be delegated to the series. + onMouseMove: function(e) { + var me = this, + position = me.getEventXY(e), + item, last, storeItem, storeField; + + if (me.mask) { + me.mixins.mask.onMouseMove.call(me, e); + } + // Ask each series if it has an item corresponding to (not necessarily exactly + // on top of) the current mouse coords. Fire itemmouseover/out events. + me.series.each(function(series) { + if (Ext.draw.Draw.withinBox(position[0], position[1], series.bbox)) { + if (series.getItemForPoint) { + item = series.getItemForPoint(position[0], position[1]); + last = series._lastItemForPoint; + storeItem = series._lastStoreItem; + storeField = series._lastStoreField; + + + if (item !== last || item && (item.storeItem != storeItem || item.storeField != storeField)) { + if (last) { + series.fireEvent('itemmouseout', last); + delete series._lastItemForPoint; + delete series._lastStoreField; + delete series._lastStoreItem; + } + if (item) { + series.fireEvent('itemmouseover', item); + series._lastItemForPoint = item; + series._lastStoreItem = item.storeItem; + series._lastStoreField = item.storeField; + } + } + } + } else { + last = series._lastItemForPoint; + if (last) { + series.fireEvent('itemmouseout', last); + delete series._lastItemForPoint; + delete series._lastStoreField; + delete series._lastStoreItem; + } + } + }, me); + }, + + // @private handle mouse leave event. + onMouseLeave: function(e) { + var me = this; + if (me.mask) { + me.mixins.mask.onMouseLeave.call(me, e); + } + me.series.each(function(series) { + delete series._lastItemForPoint; + }); + }, + + // @private buffered refresh for when we update the store + delayRefresh: function() { + var me = this; + if (!me.refreshTask) { + me.refreshTask = Ext.create('Ext.util.DelayedTask', me.refresh, me); + } + me.refreshTask.delay(me.refreshBuffer); + }, + + // @private + refresh: function() { + var me = this; + if (me.rendered && me.curWidth != undefined && me.curHeight != undefined) { + if (me.fireEvent('beforerefresh', me) !== false) { + me.redraw(); + me.fireEvent('refresh', me); + } + } + }, + + /** + * Changes the data store bound to this chart and refreshes it. + * @param {Store} store The store to bind to this chart + */ + bindStore: function(store, initial) { + var me = this; + if (!initial && me.store) { + if (store !== me.store && me.store.autoDestroy) { + me.store.destroy(); + } + else { + me.store.un('datachanged', me.refresh, me); + me.store.un('add', me.delayRefresh, me); + me.store.un('remove', me.delayRefresh, me); + me.store.un('update', me.delayRefresh, me); + me.store.un('clear', me.refresh, me); + } + } + if (store) { + store = Ext.data.StoreManager.lookup(store); + store.on({ + scope: me, + datachanged: me.refresh, + add: me.delayRefresh, + remove: me.delayRefresh, + update: me.delayRefresh, + clear: me.refresh + }); + } + me.store = store; + if (store && !initial) { + me.refresh(); + } + }, + + // @private Create Axis + initializeAxis: function(axis) { + var me = this, + chartBBox = me.chartBBox, + w = chartBBox.width, + h = chartBBox.height, + x = chartBBox.x, + y = chartBBox.y, + themeAttrs = me.themeAttrs, + config = { + chart: me + }; + if (themeAttrs) { + config.axisStyle = Ext.apply({}, themeAttrs.axis); + config.axisLabelLeftStyle = Ext.apply({}, themeAttrs.axisLabelLeft); + config.axisLabelRightStyle = Ext.apply({}, themeAttrs.axisLabelRight); + config.axisLabelTopStyle = Ext.apply({}, themeAttrs.axisLabelTop); + config.axisLabelBottomStyle = Ext.apply({}, themeAttrs.axisLabelBottom); + config.axisTitleLeftStyle = Ext.apply({}, themeAttrs.axisTitleLeft); + config.axisTitleRightStyle = Ext.apply({}, themeAttrs.axisTitleRight); + config.axisTitleTopStyle = Ext.apply({}, themeAttrs.axisTitleTop); + config.axisTitleBottomStyle = Ext.apply({}, themeAttrs.axisTitleBottom); + } + switch (axis.position) { + case 'top': + Ext.apply(config, { + length: w, + width: h, + x: x, + y: y + }); + break; + case 'bottom': + Ext.apply(config, { + length: w, + width: h, + x: x, + y: h + }); + break; + case 'left': + Ext.apply(config, { + length: h, + width: w, + x: x, + y: h + }); + break; + case 'right': + Ext.apply(config, { + length: h, + width: w, + x: w, + y: h + }); + break; + } + if (!axis.chart) { + Ext.apply(config, axis); + axis = me.axes.replace(Ext.createByAlias('axis.' + axis.type.toLowerCase(), config)); + } + else { + Ext.apply(axis, config); + } + }, + + + /** + * @private Adjust the dimensions and positions of each axis and the chart body area after accounting + * for the space taken up on each side by the axes and legend. + */ + alignAxes: function() { + var me = this, + axes = me.axes, + legend = me.legend, + edges = ['top', 'right', 'bottom', 'left'], + chartBBox, + insetPadding = me.insetPadding, + insets = { + top: insetPadding, + right: insetPadding, + bottom: insetPadding, + left: insetPadding + }; + + function getAxis(edge) { + var i = axes.findIndex('position', edge); + return (i < 0) ? null : axes.getAt(i); + } + + // Find the space needed by axes and legend as a positive inset from each edge + Ext.each(edges, function(edge) { + var isVertical = (edge === 'left' || edge === 'right'), + axis = getAxis(edge), + bbox; + + // Add legend size if it's on this edge + if (legend !== false) { + if (legend.position === edge) { + bbox = legend.getBBox(); + insets[edge] += (isVertical ? bbox.width : bbox.height) + insets[edge]; + } + } + + // Add axis size if there's one on this edge only if it has been + //drawn before. + if (axis && axis.bbox) { + bbox = axis.bbox; + insets[edge] += (isVertical ? bbox.width : bbox.height); + } + }); + // Build the chart bbox based on the collected inset values + chartBBox = { + x: insets.left, + y: insets.top, + width: me.curWidth - insets.left - insets.right, + height: me.curHeight - insets.top - insets.bottom + }; + me.chartBBox = chartBBox; + + // Go back through each axis and set its length and position based on the + // corresponding edge of the chartBBox + axes.each(function(axis) { + var pos = axis.position, + isVertical = (pos === 'left' || pos === 'right'); + + axis.x = (pos === 'right' ? chartBBox.x + chartBBox.width : chartBBox.x); + axis.y = (pos === 'top' ? chartBBox.y : chartBBox.y + chartBBox.height); + axis.width = (isVertical ? chartBBox.width : chartBBox.height); + axis.length = (isVertical ? chartBBox.height : chartBBox.width); + }); + }, + + // @private initialize the series. + initializeSeries: function(series, idx) { + var me = this, + themeAttrs = me.themeAttrs, + seriesObj, markerObj, seriesThemes, st, + markerThemes, colorArrayStyle = [], + i = 0, l, + config = { + chart: me, + seriesId: series.seriesId + }; + if (themeAttrs) { + seriesThemes = themeAttrs.seriesThemes; + markerThemes = themeAttrs.markerThemes; + seriesObj = Ext.apply({}, themeAttrs.series); + markerObj = Ext.apply({}, themeAttrs.marker); + config.seriesStyle = Ext.apply(seriesObj, seriesThemes[idx % seriesThemes.length]); + config.seriesLabelStyle = Ext.apply({}, themeAttrs.seriesLabel); + config.markerStyle = Ext.apply(markerObj, markerThemes[idx % markerThemes.length]); + if (themeAttrs.colors) { + config.colorArrayStyle = themeAttrs.colors; + } else { + colorArrayStyle = []; + for (l = seriesThemes.length; i < l; i++) { + st = seriesThemes[i]; + if (st.fill || st.stroke) { + colorArrayStyle.push(st.fill || st.stroke); + } + } + if (colorArrayStyle.length) { + config.colorArrayStyle = colorArrayStyle; + } + } + config.seriesIdx = idx; + } + if (series instanceof Ext.chart.series.Series) { + Ext.apply(series, config); + } else { + Ext.applyIf(config, series); + series = me.series.replace(Ext.createByAlias('series.' + series.type.toLowerCase(), config)); + } + if (series.initialize) { + series.initialize(); + } + }, + + // @private + getMaxGutter: function() { + var me = this, + maxGutter = [0, 0]; + me.series.each(function(s) { + var gutter = s.getGutters && s.getGutters() || [0, 0]; + maxGutter[0] = Math.max(maxGutter[0], gutter[0]); + maxGutter[1] = Math.max(maxGutter[1], gutter[1]); + }); + me.maxGutter = maxGutter; + }, + + // @private draw axis. + drawAxis: function(axis) { + axis.drawAxis(); + }, + + // @private draw series. + drawCharts: function(series) { + series.triggerafterrender = false; + series.drawSeries(); + if (!this.animate) { + series.fireEvent('afterrender'); + } + }, + + // @private remove gently. + destroy: function() { + this.surface.destroy(); + this.bindStore(null); + this.callParent(arguments); + } +});