/** * The Ext.chart package provides the capability to visualize data. * Each chart binds directly to a {@link Ext.data.Store 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 this: * * Ext.create('Ext.chart.CartesianChart', { * width: 800, * height: 600, * animation: { * easing: 'backOut', * duration: 500 * }, * store: store1, * legend: { * position: 'right' * }, * axes: [ * // ...some axes options... * ], * series: [ * // ...some series options... * ] * }); * * In this example we set the `width` and `height` of a chart; We decide whether * our series are animated or not and we select a store to be bound to the chart; * We also set the legend to the right part of the chart. * * You can register certain interactions such as {@link Ext.chart.interactions.PanZoom} * on the chart by specifying an array of names or more specific config objects. * All the events will be wired automatically. * * You can also listen to series `itemXXX` events on both chart and series level. * * For example: * * Ext.create('Ext.chart.CartesianChart', { * plugins: { * chartitemevents: { * moveEvents: true * } * }, * store: { * fields: ['pet', 'households', 'total'], * data: [ * {pet: 'Cats', households: 38, total: 93}, * {pet: 'Dogs', households: 45, total: 79}, * {pet: 'Fish', households: 13, total: 171} * ] * }, * axes: [{ * type: 'numeric', * position: 'left' * }, { * type: 'category', * position: 'bottom' * }], * series: [{ * type: 'bar', * xField: 'pet', * yField: 'households', * listeners: { * itemmousemove: function (series, item, event) { * console.log('itemmousemove', item.category, item.field); * } * } * }, { * type: 'line', * xField: 'pet', * yField: 'total', * marker: true * }], * listeners: { // Listen to itemclick events on all series. * itemclick: function (chart, item, event) { * console.log('itemclick', item.category, item.field); * } * } * }); * * Important! It's generally a poor design choice to put interactive charts * inside scrollable views, in such cases it's not possible to tell * which component should respond to the interaction. * Since charts are typically interactive their default touch action config * looks as follows: {@link Ext.draw.Container#touchAction}. * If you do have a chart inside a scrollable view, even if it has no interactions, * you have to set its `touchAction` config to the following: * * touchAction: { * panX: true, * panY: true * } * * Otherwise, if a touch action started on a chart, a swipe will not scroll * the view. * * For more information about the axes and series configurations please check * the documentation of each series (Line, Bar, Pie, etc). * */Ext.define('Ext.chart.AbstractChart', { extend: 'Ext.draw.Container', requires: [ 'Ext.chart.theme.Default', 'Ext.chart.series.Series', 'Ext.chart.interactions.Abstract', 'Ext.chart.axis.Axis', 'Ext.chart.Util', 'Ext.data.StoreManager', 'Ext.chart.legend.Legend', 'Ext.chart.legend.SpriteLegend', 'Ext.chart.Caption', 'Ext.chart.legend.store.Store', 'Ext.data.Store' ], isChart: true, defaultBindProperty: 'store', /** * @event beforerefresh * Fires before a refresh to the chart data is called. If the `beforerefresh` * handler returns `false` the {@link #refresh} action will be canceled. * @param {Ext.chart.AbstractChart} this */ /** * @event refresh * Fires after the chart data has been refreshed. * @param {Ext.chart.AbstractChart} this */ /** * @event redraw * Fires after each {@link #event!redraw} call. * @param {Ext.chart.AbstractChart} this */ /** * @private * @event layout * Fires after the final layout is done. * (Two layouts may be required to fully render a chart. * Typically for the initial render and every time thickness * of the chart's axes changes.) * @param {Ext.chart.AbstractChart} this */ /** * @event itemhighlight * Fires when an item is highlighted. * @param {Ext.chart.AbstractChart} this * @param {Object} newItem The new highlight item. * @param {Object} oldItem The old highlight item. */ /** * @event itemhighlightchange * Fires when an item's highlight changes. * @param this * @param {Object} newItem The new highlight item. * @param {Object} oldItem The old highlight item. */ /** * @event itemmousemove * Fires when the mouse is moved on a series item. * *Note*: This event requires the {@link Ext.chart.plugin.ItemEvents chartitemevents} * plugin be added to the chart. * @param {Ext.chart.AbstractChart} chart * @param {Object} item * @param {Event} event */ /** * @event itemmouseup * Fires when a mouseup event occurs on a series item. * *Note*: This event requires the {@link Ext.chart.plugin.ItemEvents chartitemevents} * plugin be added to the chart. * @param {Ext.chart.AbstractChart} chart * @param {Object} item * @param {Event} event */ /** * @event itemmousedown * Fires when a mousedown event occurs on a series item. * *Note*: This event requires the {@link Ext.chart.plugin.ItemEvents chartitemevents} * plugin be added to the chart. * @param {Ext.chart.AbstractChart} chart * @param {Object} item * @param {Event} event */ /** * @event itemmouseover * Fires when the mouse enters a series item. * *Note*: This event requires the {@link Ext.chart.plugin.ItemEvents chartitemevents} * plugin be added to the chart. * @param {Ext.chart.AbstractChart} chart * @param {Object} item * @param {Event} event */ /** * @event itemmouseout * Fires when the mouse exits a series item. * *Note*: This event requires the {@link Ext.chart.plugin.ItemEvents chartitemevents} * plugin be added to the chart. * @param {Ext.chart.AbstractChart} chart * @param {Object} item * @param {Event} event */ /** * @event itemclick * Fires when a click event occurs on a series item. * *Note*: This event requires the {@link Ext.chart.plugin.ItemEvents chartitemevents} * plugin be added to the chart. * @param {Ext.chart.AbstractChart} chart * @param {Object} item * @param {Event} event */ /** * @event itemdblclick * Fires when a double click event occurs on a series item. * *Note*: This event requires the {@link Ext.chart.plugin.ItemEvents chartitemevents} * plugin be added to the chart. * @param {Ext.chart.AbstractChart} chart * @param {Object} item * @param {Event} event */ /** * @event itemtap * Fires when a tap event occurs on a series item. * *Note*: This event requires the {@link Ext.chart.plugin.ItemEvents chartitemevents} * plugin be added to the chart. * @param {Ext.chart.AbstractChart} chart * @param {Object} item * @param {Event} event */ /** * @event storechange * Fires when the store of the chart changes. * @param {Ext.chart.AbstractChart} chart * @param {Ext.data.Store} newStore * @param {Ext.data.Store} oldStore */ config: { /** * @cfg {Ext.data.Store/String/Object} store * The data source to which the chart is bound. * Acceptable values for this property are: * * - **any {@link Ext.data.Store Store} class / subclass** * - **an {@link Ext.data.Store#storeId ID of a store}** * - **a {@link Ext.data.Store Store} config object**. When passing a config you can * specify the store type by alias. Passing a config object with a store type will * dynamically create a new store of that type when the chart is instantiated. * * For example: * * Ext.define('MyApp.store.Customer', { * extend: 'Ext.data.Store', * alias: 'store.customerstore', * * fields: ['name', 'value'] * }); * * * Ext.create({ * xtype: 'cartesian', * renderTo: document.body, * height: 400, * width: 400, * store: { * type: 'customerstore', * data: [{ * name: 'metric one', * value: 10 * }] * }, * axes: [{ * type: 'numeric', * position: 'left', * title: { * text: 'Sample Values', * fontSize: 15 * }, * fields: 'value' * }, { * type: 'category', * position: 'bottom', * title: { * text: 'Sample Values', * fontSize: 15 * }, * fields: 'name' * }], * series: { * type: 'bar', * xField: 'name', * yField: 'value' * } * }); */ store: 'ext-empty-store', /** * @cfg {String} [theme="default"] * The name of the theme to be used. A theme defines the colors and styles * used by the series, axes, markers and other chart components. * Please see the documentation for the {@link Ext.chart.theme.Base} class * for more information. * * Possible theme values are: * - 'green', 'sky', 'red', 'purple', 'blue', 'yellow' * - 'category1' to 'category6' * - and the above theme names with the '-gradients' suffix, e.g. 'green-gradients' * * IMPORTANT: You should require the themes you use; for example, to use: * * theme: 'blue' * * the `Ext.chart.theme.Blue` class should be required: * * requires: 'Ext.chart.theme.Blue' * * To require all chart themes: * * requires: 'Ext.chart.theme.*' */ theme: 'default', /** * Chart captions can be used to place titles, subtitles, credits and other captions * inside a chart. For example: * * captions: { * title: { * text: 'Consumer Price Index' * }, * subtitle: { * text: 'from 2007 to 2017' * }, * credits: { * text: 'Source: 'bls.gov' * } * } * * One can use any names for properties in the `captions` config, but the `title`, * `subtitle` and `credits` ones have a special meaning - they are automatically * themeable. The `title` and `subtitle` are automatically docked to the top of * a chart and the `credits` to the bottom. The `title` uses the largest and * the heaviest font, while the `credits` - the smallest and the lightest. * * Other captions besides those three can be easily defined as well: * * captions: { * myFancyCaption: { * docked: 'bottom', * align: 'left', * style: { * fontSize: 18, * fontWeight: 'bold', * fontFamily: 'Verdana' * } * } * } * * If a caption config only specifies text, a shorthand syntax is also possible: * * captions: { * title: 'Consumer Price Index' * } * * @cfg {Object} captions * @cfg {Ext.chart.Caption} captions.title * @cfg {Ext.chart.Caption} captions.subtitle * @cfg {Ext.chart.Caption} captions.credits */ captions: null, /** * @cfg {Object} style * The style for the chart component. */ style: null, /** * @cfg {Boolean/Object} [animation=true] * Defaults to `easeInOut` easing with a 500ms duration. * See {@link Ext.draw.modifier.Animation} for possible configuration options. */ animation: !Ext.isIE8, /** * @cfg {Ext.chart.series.Series/Array} series * Array of {@link Ext.chart.series.Series Series} instances or config objects. * For example: * * series: [{ * type: 'column', * axis: 'left', * listeners: { * 'afterrender': function() { * console.log('afterrender'); * } * }, * xField: 'category', * yField: 'data1' * }] */ series: [], /** * @cfg {Ext.chart.axis.Axis/Array/Object} axes * Array of {@link Ext.chart.axis.Axis Axis} instances or config objects. * For example: * * axes: [{ * type: 'numeric', * position: 'left', * title: 'Number of Hits', * minimum: 0 * }, { * type: 'category', * position: 'bottom', * title: 'Month of the Year' * }] */ axes: [], /** * @cfg {Ext.chart.legend.Legend/Ext.chart.legend.SpriteLegend/Boolean} legend * The legend config for the chart. If specified, a legend block will be shown * next to the chart. * Each legend item displays the {@link Ext.chart.series.Series#title title} * of the series, the color of the series and allows to toggle the visibility * of the series (at least one series should remain visible). * * Sencha Charts support two types of legends: sprite based and DOM based. * * The sprite based legend can be shown in chart {@link Ext.draw.Container#preview preview} * and is a part of the downloaded {@link Ext.draw.Container#download chart image}. * The sprite based legend is always displayed in full and takes as much space as necessary, * the legend items are split into columns to use the available space efficiently. * The sprite based legend is styled via a {@link Ext.chart.theme.Base chart theme}. * * The DOM based legend supports RTL. * It occupies a fixed width or height and scrolls when the content overflows. * The DOM based legend is styled via CSS rules. * * By default the sprite legend is used. The type can be explicitly specified: * * legend: { * type: 'dom', // 'sprite' is another possible value * docked: 'top' * } * * If the legend config is set to `true`, the sprite legend will be used * docked to the bottom. */ legend: null, /** * @cfg {Array} colors * Array of colors/gradients to override the color of items and legends. */ colors: null, /** * @cfg {Object/Number/String} insetPadding * The amount of inset padding in pixels for the chart. * Inset padding is the padding from the boundary of the chart to any * of its contents. */ insetPadding: { top: 10, left: 10, right: 10, bottom: 10 }, /** * @cfg {Object} background Set the chart background. * This can be a gradient object, image, or color. * * For example, if `background` were to be a color we could set the object as * * background: '#ccc' * * You can specify an image by using: * * background: { * type: 'image', * src: 'http://path.to.image/' * } * * Also you can specify a gradient by using the gradient object syntax: * * background: { * type: 'linear', * degrees: 0, * stops: [ * { * offset: 0, * color: 'white' * }, * { * offset: 1, * color: 'blue' * } * ] * } */ background: null, /** * @cfg {Array} interactions * Interactions are optional modules that can be plugged in to a chart * to allow the user to interact with the chart and its data in special ways. * The `interactions` config takes an Array of Object configurations, * each one corresponding to a particular interaction class identified * by a `type` property: * * new Ext.chart.AbstractChart({ * renderTo: Ext.getBody(), * width: 800, * height: 600, * store: store1, * axes: [ * // ...some axes options... * ], * series: [ * // ...some series options... * ], * interactions: [{ * type: 'interactiontype' * // ...additional configs for the interaction... * }] * }); * * When adding an interaction which uses only its default configuration * (no extra properties other than `type`), you can alternately specify * only the type as a String rather than the full Object: * * interactions: ['reset', 'rotate'] * * The current supported interaction types include: * * - {@link Ext.chart.interactions.PanZoom panzoom} - allows pan and zoom of axes * - {@link Ext.chart.interactions.ItemHighlight itemhighlight} - allows highlighting * of series data points * - {@link Ext.chart.interactions.ItemInfo iteminfo} - allows displaying details of * a data point in a popup panel * - {@link Ext.chart.interactions.Rotate rotate} - allows rotation of pie and radar series * * See the documentation for each of those interaction classes to see how they * can be configured. * * Additional custom interactions can be registered using `'interactions.'` alias prefix. */ interactions: [], /** * @private * The main area of the chart where grid and series are drawn. */ mainRect: null, /** * @private * Override value. */ resizeHandler: null, /** * @cfg {Object} highlightItem * The current highlight item in the chart. * The object must be the one that you get from item events. * * Note that series can also own highlight items. * This notion is separate from this one and should not be used at the same time. */ highlightItem: null, /* eslint-disable indent */ surfaceZIndexes: { background: 0, // Contains the backround 'rect' sprite. main: 1, // Contains grid lines and CrossZoom overlay 'rect' sprite. grid: 2, // Reserved. series: 3, // Contains series sprites. axis: 4, // No actual `axis` surface is created, but this zIndex is used // for all axis surfaces (one surface is created per axis). chart: 5, // Covers whole chart, minus the legend area. // Contains sprites defined in the `sprites` config, // title, subtitle and credits. caption: 6, // Contains title, subtitle and credits sprites. overlay: 7, // This surface will typically contain chart labels // and interaction sprites like crosshair lines. // With cartesian charts, equivalent in size to the `series` surface. // With polar charts, equivalent in size to the `chart` surface. legend: 8 // `SpriteLegend` surface. } /* eslint-enable indent */ }, /** * @private */ legendStore: null, /** * When this is non-zero, changes to sprite attributes apply instantly. * See {@link #getAnimation}. * @private */ animationSuspendCount: 0, /** * @private */ chartLayoutSuspendCount: 0, /** * @private */ chartLayoutCount: 0, /** * @private */ scheduledLayoutId: null, /** * @private */ axisThicknessSuspendCount: 0, /** * @private * Indicates that thickness of one or more axes has changed, * at the time of {@link #performLayout} call. I.e. 'performLayout' * should be called again when current layout is done. */ isThicknessChanged: false, constructor: function(config) { var me = this; me.itemListeners = {}; me.surfaceMap = {}; me.chartComponents = {}; me.isInitializing = true; me.suspendChartLayout(); me.animationSuspendCount++; me.callParent(arguments); me.isInitializing = false; me.getSurface('main'); me.getSurface('chart').setFlipRtlText(me.getInherited().rtl); me.getSurface('overlay').waitFor(me.getSurface('series')); me.animationSuspendCount--; me.resumeChartLayout(); }, applyAnimation: function(animation, oldAnimation) { return Ext.chart.Util.applyAnimation(animation, oldAnimation); }, updateAnimation: function() { if (this.isConfiguring) { return; } // eslint-disable-next-line vars-on-top var seriesList = this.getSeries(), ln = seriesList.length, i, series; this.isSettingSeriesAnimation = true; for (i = 0; i < ln; i++) { series = seriesList[i]; // Don't update the series animation config, if it was set by // a user, unless 'resumeAnimation' was called. if (!series.isUserAnimation || this.animationSuspendCount === 0) { series.setAnimation(series.getAnimation()); } } this.isSettingSeriesAnimation = false; }, getAnimation: function() { var result; if (this.animationSuspendCount) { result = { duration: 0 }; } else { result = this.callParent(); } return result; }, suspendAnimation: function() { this.animationSuspendCount++; if (this.animationSuspendCount === 1) { this.updateAnimation(); } }, resumeAnimation: function() { this.animationSuspendCount--; if (this.animationSuspendCount === 0) { this.updateAnimation(); } }, applyInsetPadding: function(padding, oldPadding) { var result; if (!Ext.isObject(padding)) { result = Ext.util.Format.parseBox(padding); } else if (!oldPadding) { result = padding; } else { result = Ext.apply(oldPadding, padding); } return result; }, /** * Suspends chart's layout. */ suspendChartLayout: function() { var me = this; me.chartLayoutSuspendCount++; if (me.chartLayoutSuspendCount === 1) { if (me.scheduledLayoutId) { me.layoutInSuspension = true; me.cancelChartLayout(); } else { me.layoutInSuspension = false; } } }, /** * Decrements chart's layout suspend count. * When the suspend count is decremented to zero, * a layout is scheduled. */ resumeChartLayout: function() { var me = this; me.chartLayoutSuspendCount--; if (me.chartLayoutSuspendCount === 0) { if (me.layoutInSuspension) { me.scheduleLayout(); } } }, /** * Cancel a scheduled layout. */ cancelChartLayout: function() { if (this.scheduledLayoutId) { Ext.draw.Animator.cancel(this.scheduledLayoutId); this.scheduledLayoutId = null; this.checkLayoutEnd(); } }, /** * Schedule a layout at next frame. */ scheduleLayout: function() { var me = this; if (me.allowSchedule() && !me.scheduledLayoutId) { me.scheduledLayoutId = Ext.draw.Animator.schedule('doScheduleLayout', me); } }, allowSchedule: function() { return true; }, doScheduleLayout: function() { var me = this; me.scheduledLayoutId = null; if (me.chartLayoutSuspendCount) { me.layoutInSuspension = true; } else { me.performLayout(); } }, /** * Prevent axes from triggering chart layout when their thickness changes. * E.g. during an interaction that makes changes to the axes, * or when chart layout was triggered by something else, * for example a chart resize event. */ suspendThicknessChanged: function() { this.axisThicknessSuspendCount++; }, /** * Decrements axis thickness suspend count. * When axis thickness suspend count is decremented to zero, * chart layout is performed. */ resumeThicknessChanged: function() { if (this.axisThicknessSuspendCount > 0) { this.axisThicknessSuspendCount--; if (this.axisThicknessSuspendCount === 0 && this.isThicknessChanged) { this.onThicknessChanged(); } } }, onThicknessChanged: function() { if (this.axisThicknessSuspendCount === 0) { this.isThicknessChanged = false; this.performLayout(); } else { this.isThicknessChanged = true; } }, applySprites: function(sprites) { var surface = this.getSurface('chart'); sprites = Ext.Array.from(sprites); surface.removeAll(true); surface.add(sprites); return sprites; }, initItems: function() { var items = this.items, i, ln, item; if (items && !items.isMixedCollection) { this.items = []; items = Ext.Array.from(items); for (i = 0, ln = items.length; i < ln; i++) { item = items[i]; if (item.type) { Ext.raise("To add custom sprites to the chart use the 'sprites' config."); } else { this.items.push(item); } } } // @noOptimize.callParent this.callParent(); // noOptimize is needed because in the ext build we have a parent method to call, // but in touch we do not so we need to suppress the cmd warning during optimized build }, applyBackground: function(newBackground, oldBackground) { var surface = this.getSurface('background'); return this.refreshBackground(surface, newBackground, oldBackground); }, /** * @private * The background updater. Used by both the chart and the sprite legend. * @param surface The surface to put the background in. * @param newBackground * @param oldBackground * @return {Ext.draw.sprite.Rect/Ext.draw.sprite.Sprite} */ refreshBackground: function(surface, newBackground, oldBackground) { var width, height, isUpdateOld; if (newBackground) { if (oldBackground) { width = oldBackground.attr.width; height = oldBackground.attr.height; isUpdateOld = oldBackground.type === (newBackground.type || 'rect'); } if (newBackground.isSprite) { oldBackground = newBackground; } else if (newBackground.type === 'image' && Ext.isString(newBackground.src)) { if (isUpdateOld) { oldBackground.setAttributes({ src: newBackground.src }); } else { surface.remove(oldBackground, true); oldBackground = surface.add(newBackground); } } else { if (isUpdateOld) { oldBackground.setAttributes({ fillStyle: newBackground }); } else { surface.remove(oldBackground, true); oldBackground = surface.add({ type: 'rect', fillStyle: newBackground, animation: { customDurations: { x: 0, y: 0, width: 0, height: 0 } } }); } } } if (width && height) { oldBackground.setAttributes({ width: width, height: height }); } oldBackground.setAnimation(this.getAnimation()); return oldBackground; }, defaultResizeHandler: function(size) { this.scheduleLayout(); return false; }, applyMainRect: function(newRect, rect) { if (!rect) { return newRect; } this.getSeries(); this.getAxes(); if (newRect[0] === rect[0] && newRect[1] === rect[1] && newRect[2] === rect[2] && newRect[3] === rect[3]) { return rect; } else { return newRect; } }, register: function(component) { var map = this.chartComponents, id = component.getId(); //<debug> if (id === undefined) { Ext.raise('Chart component id is undefined. ' + 'Please ensure the component has an id.'); } if (id in map) { Ext.raise('Registering duplicate chart component id "' + id + '"'); } //</debug> map[id] = component; }, unregister: function(component) { var map = this.chartComponents, id = component.getId(); delete map[id]; }, get: function(id) { return this.chartComponents[id]; }, /** * @method getAxis Returns an axis instance based on the type of data passed. * @param {String/Number/Ext.chart.axis.Axis} axis You may request an axis by passing * an id, the number of the array key returned by {@link #getAxes}, or an axis instance. * @return {Ext.chart.axis.Axis} The axis requested. */ getAxis: function(axis) { if (axis instanceof Ext.chart.axis.Axis) { return axis; } else if (Ext.isNumber(axis)) { return this.getAxes()[axis]; } else if (Ext.isString(axis)) { return this.get(axis); } }, getSurface: function(id, type) { var me = this, map = me.surfaceMap, surface; id = id || 'main'; type = type || id; surface = this.callParent([id, type]); if (!map[type]) { map[type] = []; } if (Ext.Array.indexOf(map[type], surface) < 0) { surface.type = type; map[type].push(surface); surface.on('destroy', me.forgetSurface, me); } return surface; }, forgetSurface: function(surface) { var map = this.surfaceMap, group, index; if (!map || this.destroying) { return; } group = map[surface.type]; index = group ? Ext.Array.indexOf(group, surface) : -1; if (index >= 0) { group.splice(index, 1); } }, applyAxes: function(newAxes, oldAxes) { var me = this, positions = { left: 'right', right: 'left' }, result = [], axis, oldAxis, linkedTo, id, oldMap, series, i, j, ln; me.animationSuspendCount++; me.getStore(); if (!oldAxes) { oldAxes = []; oldAxes.map = {}; } oldMap = oldAxes.map; result.map = {}; newAxes = Ext.Array.from(newAxes, true); for (i = 0, ln = newAxes.length; i < ln; i++) { axis = newAxes[i]; if (!axis) { continue; } if (axis instanceof Ext.chart.axis.Axis) { oldAxis = oldMap[axis.getId()]; axis.setChart(me); } else { axis = Ext.Object.chain(axis); linkedTo = axis.linkedTo; id = axis.id; if (Ext.isNumber(linkedTo)) { axis = Ext.merge({}, newAxes[linkedTo], axis); } else if (Ext.isString(linkedTo)) { Ext.Array.each(newAxes, function(item) { if (item.id === axis.linkedTo) { axis = Ext.merge({}, item, axis); return false; } }); } axis.id = id; axis.chart = me; if (me.getInherited().rtl) { axis.position = positions[axis.position] || axis.position; } id = axis.getId && axis.getId() || axis.id; axis = Ext.factory(axis, null, oldAxis = oldMap[id], 'axis'); } if (axis) { result.push(axis); result.map[axis.getId()] = axis; } } me.axesChangeSeries = {}; for (i in oldMap) { if (!result.map[i]) { oldAxis = oldMap[i]; if (oldAxis && !oldAxis.destroyed) { // At this point the series still have their `xAxis` and `yAxis` configs // set to old axes. We need to update such series with new matching axes // by calling their `onAxesChange` method. for (j = 0, ln = oldAxis.boundSeries.length; j < ln; j++) { series = oldAxis.boundSeries[j]; me.axesChangeSeries[series.getId()] = series; } oldAxis.destroy(); } } } me.animationSuspendCount--; return result; }, updateAxes: function(axes) { var me = this, seriesMap = me.axesChangeSeries, series, id, i, ln, axis; for (id in seriesMap) { series = seriesMap[id]; // `true` to force set series' axes, even if they are already set // (in this case to old axes that were just destroyed in the `axes` applier). series.onAxesChange(me, true); } // If changes to the `axes` config are made post chart creation, without making any // changes to the series afterwards, we need to figure out the new axes' `boundSeries` // manually, as the 'serieschange' event won't be fired in this case. for (i = 0, ln = axes.length; i < ln; i++) { axis = axes[i]; axis.onSeriesChange(me); } if (!me.isConfiguring && !me.destroying) { me.scheduleLayout(); } }, circularCopyArray: function(inArray, startIndex, count) { var outArray = [], i, len = inArray && inArray.length; if (len) { for (i = 0; i < count; i++) { outArray.push(inArray[(startIndex + i) % len]); } } return outArray; }, circularCopyObject: function(inObject, startIndex, count) { var me = this, name, value, outObject = {}; if (count) { for (name in inObject) { if (inObject.hasOwnProperty(name)) { value = inObject[name]; if (Ext.isArray(value)) { outObject[name] = me.circularCopyArray(value, startIndex, count); } else { outObject[name] = value; } } } } return outObject; }, getColors: function() { var me = this, configColors = me.config.colors, theme = me.getTheme(); if (Ext.isArray(configColors) && configColors.length > 0) { configColors = me.applyColors(configColors); } return configColors || (theme && theme.getColors()); }, applyColors: function(newColors) { newColors = Ext.Array.map(newColors, function(color) { if (Ext.isString(color)) { return color; } else { return color.toString(); } }); return newColors; }, updateColors: function(newColors) { var me = this, theme = me.getTheme(), colors = newColors || (theme && theme.getColors()), colorIndex = 0, series = me.getSeries(), seriesCount = series && series.length, i, seriesItem, seriesColors, seriesColorCount; if (colors.length) { for (i = 0; i < seriesCount; i++) { seriesItem = series[i]; seriesColorCount = seriesItem.themeColorCount(); seriesColors = me.circularCopyArray(colors, colorIndex, seriesColorCount); colorIndex += seriesColorCount; seriesItem.updateChartColors(seriesColors); } } if (!me.isConfiguring) { me.refreshLegendStore(); } }, applyTheme: function(theme) { if (theme && theme.isTheme) { return theme; } return Ext.Factory.chartTheme(theme); }, updateGradients: function(gradients) { if (!Ext.isEmpty(gradients)) { this.updateTheme(this.getTheme()); } }, updateTheme: function(theme, oldTheme) { var me = this, axes = me.getAxes(), series = me.getSeries(), colors = me.getColors(), i; if (!series) { return; } me.updateChartTheme(theme); for (i = 0; i < axes.length; i++) { axes[i].updateTheme(theme); } for (i = 0; i < series.length; i++) { series[i].setTheme(theme); } me.updateSpriteTheme(theme); me.updateColors(colors); // It may be necessary to perform a layout here. // But instead of the 'chart.scheduleLayout' call, we can call // 'chart.redraw'. If after the redraw call the thickness // of any axis changes, this will automatically trigger // chart layout (see Ext.chart.axis.sprite.Axis.doThicknessChanged). // Otherwise, no layout is necessary. me.redraw(); me.fireEvent('themechange', me, theme, oldTheme); }, themeOnlyIfConfigured: { captions: true }, updateChartTheme: function(theme) { var me = this, chartTheme = theme.getChart(), initialConfig = me.getInitialConfig(), defaultConfig = me.defaultConfig, configs = me.self.getConfigurator().configs, genericChartTheme = chartTheme.defaults, specificChartTheme = chartTheme[me.xtype], themeOnlyIfConfigured = me.themeOnlyIfConfigured, key, value, isObjValue, isUnusedConfig, initialValue, cfg; chartTheme = Ext.merge({}, genericChartTheme, specificChartTheme); for (key in chartTheme) { value = chartTheme[key]; cfg = configs[key]; if (value !== null && value !== undefined && cfg) { initialValue = initialConfig[key]; isObjValue = Ext.isObject(value); isUnusedConfig = initialValue === defaultConfig[key]; if (isObjValue) { if (isUnusedConfig && themeOnlyIfConfigured[key]) { continue; } value = Ext.merge({}, value, initialValue); } if (isUnusedConfig || isObjValue) { me[cfg.names.set](value); } } } }, updateSpriteTheme: function(theme) { var me = this, chartSurface, sprites, styles, sprite, style, key, attr, isText, i, ln; me.getSprites(); chartSurface = me.getSurface('chart'); sprites = chartSurface.getItems(); styles = theme.getSprites(); for (i = 0, ln = sprites.length; i < ln; i++) { sprite = sprites[i]; style = styles[sprite.type]; if (style) { attr = {}; isText = sprite.type === 'text'; for (key in style) { if (!(key in sprite.config)) { // Setting individual font attributes will take over the 'font' shorthand // attribute, but this behavior is undesireable for theming. if (!(isText && key.indexOf('font') === 0 && sprite.config.font)) { attr[key] = style[key]; } } } sprite.setAttributes(attr); } } }, /** * Adds a {@link Ext.chart.series.Series Series} to this chart. * * The Series (or array) passed will be added to the existing series. If an `id` is specified * in a new Series, any existing Series of that `id` will be updated. * * The chart will be redrawn in response to the change. * * @param {Object/Object[]/Ext.chart.series.Series/Ext.chart.series.Series[]} newSeries A * config object describing the Series to add, or an instantiated Series object. Or an array * of these. */ addSeries: function(newSeries) { var series = this.getSeries(); series = series.concat(Ext.Array.from(newSeries)); this.setSeries(series); }, /** * Remove a {@link Ext.chart.series.Series Series} from this chart. * The Series (or array) passed will be removed from the existing series. * * The chart will be redrawn in response to the change. * * @param {Ext.chart.series.Series/String} series The Series or the `id` of the Series * to remove. May be an array. */ removeSeries: function(series) { var existingSeries = this.getSeries(), newSeries = [], removeMap = {}, i, len, s; series = Ext.Array.from(series); // Build a map of the Series IDs that are to be removed for (i = 0, len = series.length; i < len; i++) { s = series[i]; // If they passed a Series Object if (typeof s !== 'string') { s = s.getId(); } removeMap[s] = true; } // Build a new Series array that excludes those Series scheduled for removal for (i = 0, len = existingSeries.length; i < len; i++) { if (!removeMap[existingSeries[i].getId()]) { newSeries.push(existingSeries[i]); } } this.setSeries(newSeries); }, applySeries: function(newSeries, oldSeries) { var me = this, result = [], oldMap, oldSeriesItem, i, ln, series; me.animationSuspendCount++; me.getAxes(); if (oldSeries) { oldMap = oldSeries.map; } else { oldSeries = []; oldMap = oldSeries.map = {}; } result.map = {}; newSeries = Ext.Array.from(newSeries, true); for (i = 0, ln = newSeries.length; i < ln; i++) { series = newSeries[i]; if (!series) { continue; } oldSeriesItem = oldMap[series.getId && series.getId() || series.id]; // New Series instance passed in if (series instanceof Ext.chart.series.Series) { // Replacing if (oldSeriesItem && oldSeriesItem !== series) { oldSeriesItem.destroy(); } series.setChart(me); } // Series config object passed in else if (Ext.isObject(series)) { // Config object matched an existing Series item by id; // update its configuration if (oldSeriesItem) { oldSeriesItem.setConfig(series); series = oldSeriesItem; } // Create a new Series else { if (Ext.isString(series)) { series = { type: series }; } series.chart = me; series = Ext.create(series.xclass || ('series.' + series.type), series); } } result.push(series); result.map[series.getId()] = series; } for (i in oldMap) { if (!result.map[oldMap[i].id]) { oldMap[i].destroy(); } } me.animationSuspendCount--; return result; }, updateSeries: function(newSeries, oldSeries) { var me = this; if (me.destroying) { return; } me.animationSuspendCount++; me.fireEvent('serieschange', me, newSeries, oldSeries); if (!Ext.isEmpty(newSeries)) { me.updateTheme(me.getTheme()); } me.refreshLegendStore(); if (!me.isConfiguring && !me.destroying) { me.scheduleLayout(); } me.animationSuspendCount--; }, defaultLegendType: 'sprite', applyLegend: function(legend, oldLegend) { var me = this, result = null, alias; if (oldLegend && !(oldLegend.destroyed || oldLegend.destroying)) { if (me.legendStoreListeners) { me.legendStoreListeners.destroy(); } if (me.legendStore) { me.legendStore.destroy(); } oldLegend.destroy(); } if (legend) { if (Ext.isBoolean(legend)) { result = Ext.create('legend.' + me.defaultLegendType, { docked: 'bottom', chart: me }); } else { legend.docked = legend.docked || 'bottom'; legend.chart = me; alias = 'legend.' + (legend.type || me.defaultLegendType); result = Ext.create(alias, legend); } } return result; }, updateLegend: function(legend) { var me = this; // Probably has been already destroyed with the old legend, // but making sure. me.destroyLegendStore(); if (legend) { me.getItems(); legend.setStore(me.refreshLegendStore()); } if (!me.isConfiguring) { me.scheduleLayout(); } }, captionApplier: function(caption, oldCaption) { var me = this, result; if (oldCaption && !(oldCaption.destroyed || oldCaption.destroying)) { oldCaption.destroy(); } if (caption) { caption.chart = me; result = new Ext.chart.Caption(caption); } return result; }, applyCaptions: function(captions, oldCaptions) { var map = {}, caption, oldCaption, name, any; for (name in captions) { caption = captions[name]; if (caption && !caption.length && !(caption.text && caption.text.length)) { caption = null; } else if (typeof caption === 'string') { caption = { text: caption }; // Initial config is used for proper theming (see `updateChartTheme`) // and config merging, however, mergin won't work as expected, if // the initial config value remains a string, so we modify it here. this.getInitialConfig().captions[name] = caption; } oldCaption = oldCaptions && oldCaptions[name]; caption = this.captionApplier(caption, oldCaption); if (caption) { any = true; map[name] = caption; } } return any && map; }, updateCaptions: function() { var me = this; if (!me.isConfiguring) { me.scheduleLayout(); } }, /** * Return the legend store that contains all the legend information. * This information is collected from all the series. * @return {Ext.chart.legend.store.Store} */ getLegendStore: function() { var me = this, store = me.legendStore; if (!store) { store = me.legendStore = new Ext.chart.legend.store.Store({ chart: me }); me.legendStoreListeners = store.on({ scope: me, update: 'onLegendStoreUpdate', destroyable: true }); } return store; }, destroyLegendStore: function() { var store = this.legendStore; if (store && !(store.destroyed || store.destroying)) { store.destroy(); } this.legendStore = null; }, refreshLegendStore: function() { var me = this, legendStore = me.getLegendStore(), series, seriesList, legendData, i, ln; if (legendStore) { seriesList = me.getSeries(); legendData = []; for (i = 0, ln = seriesList.length; i < ln; i++) { series = seriesList[i]; if (series.getShowInLegend()) { series.provideLegendInfo(legendData); } } legendStore.setData(legendData); } return legendStore; }, onLegendStoreUpdate: function(store, record) { var me = this, series; if (record) { series = this.getSeries().map[record.get('series')]; if (series) { series.setHiddenByIndex(record.get('index'), record.get('disabled')); me.redraw(); } } }, applyInteractions: function(interactions, oldInteractions) { var me = this, result = [], oldMap, interaction, i, ln; interactions = Ext.Array.from(interactions, true); if (!oldInteractions) { oldInteractions = []; oldInteractions.map = {}; } oldMap = oldInteractions.map; result.map = {}; for (i = 0, ln = interactions.length; i < ln; i++) { interaction = interactions[i]; if (!interaction) { continue; } // eslint-disable-next-line max-len interaction = Ext.factory(interaction, null, oldMap[interaction.getId && interaction.getId() || interaction.id], 'interaction'); if (interaction) { interaction.setChart(me); result.push(interaction); result.map[interaction.getId()] = interaction; } } for (i in oldMap) { if (!result.map[i]) { oldMap[i].destroy(); } } return result; }, /** * Get an interaction by type. * @param {String} type The type of the interaction. * @return {Ext.chart.interactions.Abstract} The interaction. `null` * if not found. */ getInteraction: function(type) { var interactions = this.getInteractions(), len = interactions && interactions.length, out = null, interaction, i; if (len) { for (i = 0; i < len; ++i) { interaction = interactions[i]; if (interaction.type === type) { out = interaction; break; } } } return out; }, applyStore: function(store) { return store && Ext.StoreManager.lookup(store); }, updateStore: function(newStore, oldStore) { var me = this; if (oldStore && !oldStore.destroyed) { oldStore.un({ datachanged: 'onDataChanged', update: 'onDataChanged', scope: me, order: 'after' }); if (oldStore.autoDestroy) { oldStore.destroy(); } } if (newStore) { newStore.on({ datachanged: 'onDataChanged', update: 'onDataChanged', scope: me, order: 'after' }); } me.fireEvent('storechange', me, newStore, oldStore); me.onDataChanged(); }, /** * Redraw the chart. If animations are set this will animate the chart too. * Note: the actual redraw is performed in a subclass. */ redraw: function() { this.fireEvent('redraw', this); }, /** * @private * Lays out chart components and triggers a {@link #event!redraw}. * Note: the actual layout is performed in a subclass. * A subclass should not perform a layout, if this parent method * returns `false`. * @return {Boolean} */ performLayout: function() { if (this.destroying || this.destroyed) { //<debug> Ext.raise('Attempting to lay out a dead chart: ' + this.getId()); //</debug> return false; // Cancel subclass layout. } // eslint-disable-next-line vars-on-top var me = this, legend = me.getLegend(), chartRect = me.getChartRect(true), background = me.getBackground(), result = true, legendRect; me.cancelChartLayout(); //<debug> // Unlike the 'layout' event that is called after all chart layouts are done // and none are pending, this event fires before the start of each layout. me.fireEvent('beforelayout', me); //</debug> if (background) { me.getSurface('background').setRect(chartRect.slice()); background.setAttributes({ width: chartRect[2], height: chartRect[3] }); } // The top docked legend is a special case and should be laid out after captions. if (legend && legend.isSpriteLegend && !legend.isTop) { legendRect = legend.computeRect(chartRect); } me.layoutCaptions(chartRect); if (legend && legend.isSpriteLegend && legend.isTop) { legendRect = legend.computeRect(chartRect); } if (legendRect) { me.getSurface('legend').setRect(legendRect); result = legend.performLayout(); } me.getSurface('chart').setRect(chartRect); if (result) { me.hasFirstLayout = true; } return result; }, layoutCaptions: function(chartRect) { var captions = this.getCaptions(), shrinkRect = { left: 0, top: 0, right: chartRect[2], bottom: chartRect[3] }, caption, captionName, captionList, i, ln; if (captions) { captionList = []; for (captionName in captions) { captionList.push(captions[captionName]); } captionList.sort(function(a, b) { return a.getWeight() - b.getWeight(); }); for (i = 0, ln = captionList.length; i < ln; i++) { caption = captionList[i]; if (!i) { this.getSurface(caption.surfaceName).setRect(chartRect.slice()); } caption.computeRect(chartRect, shrinkRect); } this.captionList = captionList; } }, /** * @private */ checkLayoutEnd: function() { // not running not pending if (!this.chartLayoutCount && !this.scheduledLayoutId) { this.onLayoutEnd(); } }, /** * @private */ onLayoutEnd: function() { var me = this; me.fireEvent('layout', me); }, /** * @private * The area of the chart minus the legend, title, subtitle and credits. * Cache chart rect as element.getSize() results in * a relatively expensive call to the getComputedStyle(). */ getChartRect: function(isRecompute) { var me = this, chartRect, bodySize; if (isRecompute) { me.chartRect = null; } if (me.chartRect) { chartRect = me.chartRect; } else { bodySize = me.bodyElement.getSize(); chartRect = me.chartRect = [0, 0, bodySize.width, bodySize.height]; } return chartRect; }, /** * @private * Converts page coordinates into chart's 'series' surface coordinates. */ getEventXY: function(e) { return this.getSurface('series').getEventXY(e); }, /** * Given an x/y point relative to the chart, find and return the first series item that * matches that point. * @param {Number} x * @param {Number} y * @return {Object} An object with `series` and `item` properties, or `false` if no item found. */ getItemForPoint: function(x, y) { var me = this, seriesList = me.getSeries(), rect = me.getMainRect(), ln = seriesList.length, minDistance = Infinity, result = null, i, item; // The x,y here are already converted to the 'main' surface coordinates. // Series surface rect matches the main surface rect. if (!(me.hasFirstLayout && rect && x >= 0 && x <= rect[2] && y >= 0 && y <= rect[3])) { return null; } // Iterate in reverse order so that the series that render later (on top) // get hit tested first. for (i = ln - 1; i >= 0; i--) { item = seriesList[i].getItemForPoint(x, y); if (item) { // Imagine a chart with multiple series, e.g. 'line', 'scatter' and 'bar'. // For 'line' and 'scatter' series, the method will look for the nearest // marker, but for 'bar' series, it will look for the first bar that // contains the given point. For such series, the 'distance' information // is absent and meaningless. if (!item.distance) { result = item; break; } if (item.distance < minDistance) { minDistance = item.distance; result = item; } } } return result; }, /** * @private * Given an x/y point relative to the chart, find and return all series items that match * that point. * @param {Number} x * @param {Number} y * @return {Array} An array of objects with `series` and `item` properties. * @deprecated 6.5.2 This method is deprecated */ getItemsForPoint: function(x, y) { var me = this, seriesList = me.getSeries(), ln = seriesList.length, // If we haven't drawn yet, don't attempt to find any items. i = me.hasFirstLayout ? ln - 1 : -1, items = [], series, item; // Iterate from the end so that the series that are drawn later get hit tested first. for (; i >= 0; i--) { series = seriesList[i]; item = series.getItemForPoint(x, y); if (item && (item.category === 'items' || item.category === 'markers')) { items.push(item); } } return items; }, /** * @private */ onDataChanged: function() { var me = this, rect, store, series, axes; if (me.isInitializing) { return; } rect = me.getMainRect(); store = me.getStore(); series = me.getSeries(); axes = me.getAxes(); if (!store || !axes || !series) { return; } if (!rect) { // The chart hasn't been rendered yet. me.on({ redraw: me.onDataChanged, scope: me, single: true }); return; } me.processData(); me.redraw(); }, /** * @private * The number of records in the chart's store last time the data was changed. */ recordCount: 0, /** * @private */ processData: function() { var me = this, recordCount = me.getStore().getCount(), seriesList = me.getSeries(), ln = seriesList.length, isNeedUpdateColors = false, i = 0, series; for (; i < ln; i++) { series = seriesList[i]; series.processData(); if (!isNeedUpdateColors && series.isStoreDependantColorCount) { isNeedUpdateColors = true; } } if (isNeedUpdateColors && recordCount > me.recordCount) { me.updateColors(me.getColors()); me.recordCount = recordCount; } // 'refreshLegendStore' will attemp to grab the 'series', // which are still configuring at this point. // The legend store will be refreshed inside the chart.series // updater anyway. if (!me.isConfiguring) { me.refreshLegendStore(); } }, /** * Changes the data store bound to this chart and refreshes it. * @param {Ext.data.Store} store The store to bind to this chart. */ bindStore: function(store) { this.setStore(store); }, applyHighlightItem: function(newHighlightItem, oldHighlightItem) { var i1, i2, s1, s2; if (newHighlightItem === oldHighlightItem) { return; } if (Ext.isObject(newHighlightItem) && Ext.isObject(oldHighlightItem)) { i1 = newHighlightItem; i2 = oldHighlightItem; s1 = i1.sprite && (i1.sprite[0] || i1.sprite); s2 = i2.sprite && (i2.sprite[0] || i2.sprite); if (s1 === s2 && i1.index === i2.index) { return; } } return newHighlightItem; }, updateHighlightItem: function(newHighlightItem, oldHighlightItem) { var newHighlight, oldHighlight; if (oldHighlightItem) { oldHighlight = oldHighlightItem.series.getHighlight(); if (oldHighlight) { oldHighlightItem.series.setAttributesForItem(oldHighlightItem, { highlighted: false }); } } if (newHighlightItem) { newHighlight = newHighlightItem.series.getHighlight(); if (newHighlight) { newHighlightItem.series.setAttributesForItem(newHighlightItem, { highlighted: true }); } } if (oldHighlight || newHighlight) { this.fireEvent('itemhighlight', this, newHighlightItem, oldHighlightItem); } }, destroyChart: function() { var me = this; // The order is important here. me.setInteractions(null); me.setAxes(null); me.setSeries(null); me.setLegend(null); me.setStore(null); me.cancelChartLayout(); }, /* --------------------------------- Methods needed for ComponentQuery ---------------------------------- */ /** * @private * @param {Boolean} deep * @return {Array} */ getRefItems: function(deep) { var me = this, series = me.getSeries(), axes = me.getAxes(), interaction = me.getInteractions(), legend = me.getLegend(), ans = [], i, ln; for (i = 0, ln = series.length; i < ln; i++) { ans.push(series[i]); if (series[i].getRefItems) { ans.push.apply(ans, series[i].getRefItems(deep)); } } for (i = 0, ln = axes.length; i < ln; i++) { ans.push(axes[i]); if (axes[i].getRefItems) { ans.push.apply(ans, axes[i].getRefItems(deep)); } } for (i = 0, ln = interaction.length; i < ln; i++) { ans.push(interaction[i]); if (interaction[i].getRefItems) { ans.push.apply(ans, interaction[i].getRefItems(deep)); } } if (legend) { ans.push(legend); } return ans; }});