/** * @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: *

    var chart = new Ext.chart.Chart({
        renderTo: Ext.getBody(),
        width: 800,
        height: 600,
        animate: true,
        store: store1,
        renderTo: Ext.getBody(),
        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: { themeMgr: 'Ext.chart.theme.Theme' }, requires: [ 'Ext.util.MixedCollection', 'Ext.data.StoreMgr', 'Ext.chart.Legend', 'Ext.util.DelayedTask' ], /* End Definitions */ // @private viewBox: false,
/** * @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} implOrder * Defines the priority order for which Surface implementation to use. The first * one supported by the current environment will be used. */ implOrder: ['SVG', 'VML', 'Canvas'],
/** * @cfg {string} background (optional) Set the chart background. This can be a gradient name, image, or color. * Defaults to false for no background. */ background: false, 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; } } this.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: { x: 1, y: 1 } }); me.maxGutter = [0, 0]; me.store = Ext.data.StoreMgr.lookup(me.store); axes = me.axes; me.axes = new Ext.util.MixedCollection(false, function(a) { return a.position; }); if (axes) { me.axes.addAll(axes); } series = me.series; me.series = new 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 = new 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); // Relayout once for zoom if (me.ownerCt && me.ownerCt.layout && !me.zoom.adj && (me.zoom.x !== 1 || me.zoom.y !== 1)) { me.zoom.adj = true; me.ownerCt.doLayout(); } },
/** * 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 * me.zoom.y), width: (me.curWidth * me.zoom.x) }, 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); // 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; Ext.chart.Chart.superclass.afterRender.call(me); 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. onMouseDown: function(e) { var me = this, item, position; // 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 (series.getItemForPoint) { position = position || me.getEventXY(e); item = series.getItemForPoint(position[0], position[1]); if (item) { me.fireEvent('itemmousedown', item); } } }, me); }, // @private wrap the mouse up event to delegate it to the series. onMouseUp: function(e) { var me = this, item, position; // 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 (series.getItemForPoint) { position = position || me.getEventXY(e); item = series.getItemForPoint(position[0], position[1]); if (item) { me.fireEvent('itemmouseup', item); } } }, me); }, // @private wrap the mouse move event so it can be delegated to the series. onMouseMove: function(e) { var me = this, item, position, last, storeItem, storeField; // 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 (series.getItemForPoint) { position = position || me.getEventXY(e); 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) { me.fireEvent('itemmouseout', last); delete series._lastItemForPoint; delete series._lastStoreField; delete series._lastStoreItem; } if (item) { me.fireEvent('itemmouseover', item); series._lastItemForPoint = item; series._lastStoreItem = item.storeItem; series._lastStoreField = item.storeField; } } } }, me); }, // @private handle mouse leave event. onMouseLeave: function(e) { this.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 = new 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.StoreMgr.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.create('Ext.chart.axis.' + axis.type, config)); } else { Ext.apply(axis, config); } axis.drawAxis(true); }, /** * @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 if (axis) { 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 * me.zoom.x) - insets.left - insets.right, height: (me.curHeight * me.zoom.y) - 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.chart) { Ext.applyIf(config, series); series = me.series.replace(Ext.create('Ext.chart.series.' + Ext.String.capitalize(series.type), config)); } else { Ext.apply(series, config); } }, // @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.drawSeries(); }, // @private remove gently. destroy: function() { this.surface.destroy(); this.callParent(arguments); } });