/* * * * (c) 2010-2020 Torstein Honsi * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ 'use strict'; import H from './Globals.js'; /** * Callback for chart constructors. * * @callback Highcharts.ChartCallbackFunction * * @param {Highcharts.Chart} chart * Created chart. */ /** * Format a number and return a string based on input settings. * * @callback Highcharts.NumberFormatterCallbackFunction * * @param {number} number * The input number to format. * * @param {number} decimals * The amount of decimals. A value of -1 preserves the amount in the * input number. * * @param {string} [decimalPoint] * The decimal point, defaults to the one given in the lang options, or * a dot. * * @param {string} [thousandsSep] * The thousands separator, defaults to the one given in the lang * options, or a space character. * * @return {string} The formatted number. */ /** * The chart title. The title has an `update` method that allows modifying the * options directly or indirectly via `chart.update`. * * @interface Highcharts.TitleObject * @extends Highcharts.SVGElement */ /** * Modify options for the title. * * @function Highcharts.TitleObject#update * * @param {Highcharts.TitleOptions} titleOptions * Options to modify. * * @param {boolean} [redraw=true] * Whether to redraw the chart after the title is altered. If doing more * operations on the chart, it is a good idea to set redraw to false and * call {@link Chart#redraw} after. */ /** * The chart subtitle. The subtitle has an `update` method that * allows modifying the options directly or indirectly via * `chart.update`. * * @interface Highcharts.SubtitleObject * @extends Highcharts.SVGElement */ /** * Modify options for the subtitle. * * @function Highcharts.SubtitleObject#update * * @param {Highcharts.SubtitleOptions} subtitleOptions * Options to modify. * * @param {boolean} [redraw=true] * Whether to redraw the chart after the subtitle is altered. If doing * more operations on the chart, it is a good idea to set redraw to false * and call {@link Chart#redraw} after. */ /** * The chart caption. The caption has an `update` method that * allows modifying the options directly or indirectly via * `chart.update`. * * @interface Highcharts.CaptionObject * @extends Highcharts.SVGElement */ /** * Modify options for the caption. * * @function Highcharts.CaptionObject#update * * @param {Highcharts.CaptionOptions} captionOptions * Options to modify. * * @param {boolean} [redraw=true] * Whether to redraw the chart after the caption is altered. If doing * more operations on the chart, it is a good idea to set redraw to false * and call {@link Chart#redraw} after. */ import Legend from './Legend.js'; import MSPointer from './MSPointer.js'; import Pointer from './Pointer.js'; import Time from './Time.js'; import U from './Utilities.js'; var addEvent = U.addEvent, animate = U.animate, animObject = U.animObject, attr = U.attr, createElement = U.createElement, css = U.css, defined = U.defined, discardElement = U.discardElement, erase = U.erase, error = U.error, extend = U.extend, find = U.find, fireEvent = U.fireEvent, getStyle = U.getStyle, isArray = U.isArray, isFunction = U.isFunction, isNumber = U.isNumber, isObject = U.isObject, isString = U.isString, merge = U.merge, numberFormat = U.numberFormat, objectEach = U.objectEach, pick = U.pick, pInt = U.pInt, relativeLength = U.relativeLength, removeEvent = U.removeEvent, setAnimation = U.setAnimation, splat = U.splat, syncTimeout = U.syncTimeout, uniqueKey = U.uniqueKey; import './Axis.js'; import './Options.js'; import './Pointer.js'; var doc = H.doc, Axis = H.Axis, // @todo add as requirement defaultOptions = H.defaultOptions, charts = H.charts, marginNames = H.marginNames, seriesTypes = H.seriesTypes, win = H.win; /* eslint-disable no-invalid-this, valid-jsdoc */ /** * The Chart class. The recommended constructor is {@link Highcharts#chart}. * * @example * var chart = Highcharts.chart('container', { * title: { * text: 'My chart' * }, * series: [{ * data: [1, 3, 2, 4] * }] * }) * * @class * @name Highcharts.Chart * * @param {string|Highcharts.HTMLDOMElement} [renderTo] * The DOM element to render to, or its id. * * @param {Highcharts.Options} options * The chart options structure. * * @param {Highcharts.ChartCallbackFunction} [callback] * Function to run when the chart has loaded and and all external images * are loaded. Defining a * [chart.events.load](https://api.highcharts.com/highcharts/chart.events.load) * handler is equivalent. */ var Chart = H.Chart = function () { this.getArgs.apply(this, arguments); }; /** * Factory function for basic charts. * * @example * // Render a chart in to div#container * var chart = Highcharts.chart('container', { * title: { * text: 'My chart' * }, * series: [{ * data: [1, 3, 2, 4] * }] * }); * * @function Highcharts.chart * * @param {string|Highcharts.HTMLDOMElement} [renderTo] * The DOM element to render to, or its id. * * @param {Highcharts.Options} options * The chart options structure. * * @param {Highcharts.ChartCallbackFunction} [callback] * Function to run when the chart has loaded and and all external images * are loaded. Defining a * [chart.events.load](https://api.highcharts.com/highcharts/chart.events.load) * handler is equivalent. * * @return {Highcharts.Chart} * Returns the Chart object. */ H.chart = function (a, b, c) { return new Chart(a, b, c); }; extend(Chart.prototype, /** @lends Highcharts.Chart.prototype */ { // Hook for adding callbacks in modules callbacks: [], /** * Handle the arguments passed to the constructor. * * @private * @function Highcharts.Chart#getArgs * * @param {...Array<*>} arguments * All arguments for the constructor. * * @return {Array<*>} * Passed arguments without renderTo. * * @fires Highcharts.Chart#event:init * @fires Highcharts.Chart#event:afterInit */ getArgs: function () { var args = [].slice.call(arguments); // Remove the optional first argument, renderTo, and // set it on this. if (isString(args[0]) || args[0].nodeName) { this.renderTo = args.shift(); } this.init(args[0], args[1]); }, /** * Overridable function that initializes the chart. The constructor's * arguments are passed on directly. * * @function Highcharts.Chart#init * * @param {Highcharts.Options} userOptions * Custom options. * * @param {Function} [callback] * Function to run when the chart has loaded and and all external * images are loaded. * * @return {void} * * @fires Highcharts.Chart#event:init * @fires Highcharts.Chart#event:afterInit */ init: function (userOptions, callback) { // Handle regular options var options, // skip merging data points to increase performance seriesOptions = userOptions.series, userPlotOptions = userOptions.plotOptions || {}; // Fire the event with a default function fireEvent(this, 'init', { args: arguments }, function () { userOptions.series = null; options = merge(defaultOptions, userOptions); // do the merge var optionsChart = options.chart || {}; // Override (by copy of user options) or clear tooltip options // in chart.options.plotOptions (#6218) objectEach(options.plotOptions, function (typeOptions, type) { if (isObject(typeOptions)) { // #8766 typeOptions.tooltip = (userPlotOptions[type] && // override by copy: merge(userPlotOptions[type].tooltip)) || void 0; // or clear } }); // User options have higher priority than default options // (#6218). In case of exporting: path is changed options.tooltip.userOptions = (userOptions.chart && userOptions.chart.forExport && userOptions.tooltip.userOptions) || userOptions.tooltip; // set back the series data options.series = userOptions.series = seriesOptions; /** * The original options given to the constructor or a chart factory * like {@link Highcharts.chart} and {@link Highcharts.stockChart}. * * @name Highcharts.Chart#userOptions * @type {Highcharts.Options} */ this.userOptions = userOptions; var chartEvents = optionsChart.events; this.margin = []; this.spacing = []; // Pixel data bounds for touch zoom this.bounds = { h: {}, v: {} }; // An array of functions that returns labels that should be // considered for anti-collision this.labelCollectors = []; this.callback = callback; this.isResizing = 0; /** * The options structure for the chart after merging * {@link #defaultOptions} and {@link #userOptions}. It contains * members for the sub elements like series, legend, tooltip etc. * * @name Highcharts.Chart#options * @type {Highcharts.Options} */ this.options = options; /** * All the axes in the chart. * * @see Highcharts.Chart.xAxis * @see Highcharts.Chart.yAxis * * @name Highcharts.Chart#axes * @type {Array} */ this.axes = []; /** * All the current series in the chart. * * @name Highcharts.Chart#series * @type {Array} */ this.series = []; /** * The `Time` object associated with the chart. Since v6.0.5, * time settings can be applied individually for each chart. If * no individual settings apply, the `Time` object is shared by * all instances. * * @name Highcharts.Chart#time * @type {Highcharts.Time} */ this.time = userOptions.time && Object.keys(userOptions.time).length ? new Time(userOptions.time) : H.time; /** * Callback function to override the default function that formats * all the numbers in the chart. Returns a string with the formatted * number. * * @name Highcharts.Chart#numberFormatter * @type {Highcharts.NumberFormatterCallbackFunction} */ this.numberFormatter = optionsChart.numberFormatter || numberFormat; /** * Whether the chart is in styled mode, meaning all presentatinoal * attributes are avoided. * * @name Highcharts.Chart#styledMode * @type {boolean} */ this.styledMode = optionsChart.styledMode; this.hasCartesianSeries = optionsChart.showAxes; var chart = this; /** * Index position of the chart in the {@link Highcharts#charts} * property. * * @name Highcharts.Chart#index * @type {number} * @readonly */ chart.index = charts.length; // Add the chart to the global lookup charts.push(chart); H.chartCount++; // Chart event handlers if (chartEvents) { objectEach(chartEvents, function (event, eventType) { if (isFunction(event)) { addEvent(chart, eventType, event); } }); } /** * A collection of the X axes in the chart. * * @name Highcharts.Chart#xAxis * @type {Array} */ chart.xAxis = []; /** * A collection of the Y axes in the chart. * * @name Highcharts.Chart#yAxis * @type {Array} * * @todo * Make events official: Fire the event `afterInit`. */ chart.yAxis = []; chart.pointCount = chart.colorCounter = chart.symbolCounter = 0; // Fire after init but before first render, before axes and series // have been initialized. fireEvent(chart, 'afterInit'); chart.firstRender(); }); }, /** * Internal function to unitialize an individual series. * * @private * @function Highcharts.Chart#initSeries * * @param {Highcharts.SeriesOptions} options * * @return {Highcharts.Series} */ initSeries: function (options) { var chart = this, optionsChart = chart.options.chart, type = (options.type || optionsChart.type || optionsChart.defaultSeriesType), series, Constr = seriesTypes[type]; // No such series type if (!Constr) { error(17, true, chart, { missingModuleFor: type }); } series = new Constr(); series.init(this, options); return series; }, /** * Internal function to set data for all series with enabled sorting. * * @private * @function Highcharts.Chart#setSeriesData * * @param {Highcharts.SeriesOptions} options * * @return {void} */ setSeriesData: function () { this.getSeriesOrderByLinks().forEach(function (series) { // We need to set data for series with sorting after series init if (!series.points && !series.data && series.enabledDataSorting) { series.setData(series.options.data, false); } }); }, /** * Sort and return chart series in order depending on the number of linked * series. * * @private * @function Highcharts.Series#getSeriesOrderByLinks * * @return {Array} */ getSeriesOrderByLinks: function () { return this.series.concat().sort(function (a, b) { if (a.linkedSeries.length || b.linkedSeries.length) { return b.linkedSeries.length - a.linkedSeries.length; } return 0; }); }, /** * Order all series above a given index. When series are added and ordered * by configuration, only the last series is handled (#248, #1123, #2456, * #6112). This function is called on series initialization and destroy. * * @private * @function Highcharts.Series#orderSeries * @param {number} [fromIndex] * If this is given, only the series above this index are handled. * @return {void} */ orderSeries: function (fromIndex) { var series = this.series, i = fromIndex || 0; for (; i < series.length; i++) { if (series[i]) { /** * Contains the series' index in the `Chart.series` array. * * @name Highcharts.Series#index * @type {number} * @readonly */ series[i].index = i; series[i].name = series[i].getName(); } } }, /** * Check whether a given point is within the plot area. * * @function Highcharts.Chart#isInsidePlot * * @param {number} plotX * Pixel x relative to the plot area. * * @param {number} plotY * Pixel y relative to the plot area. * * @param {boolean} [inverted] * Whether the chart is inverted. * * @return {boolean} * Returns true if the given point is inside the plot area. */ isInsidePlot: function (plotX, plotY, inverted) { var x = inverted ? plotY : plotX, y = inverted ? plotX : plotY, e = { x: x, y: y, isInsidePlot: x >= 0 && x <= this.plotWidth && y >= 0 && y <= this.plotHeight }; fireEvent(this, 'afterIsInsidePlot', e); return e.isInsidePlot; }, /** * Redraw the chart after changes have been done to the data, axis extremes * chart size or chart elements. All methods for updating axes, series or * points have a parameter for redrawing the chart. This is `true` by * default. But in many cases you want to do more than one operation on the * chart before redrawing, for example add a number of points. In those * cases it is a waste of resources to redraw the chart for each new point * added. So you add the points and call `chart.redraw()` after. * * @function Highcharts.Chart#redraw * * @param {boolean|Highcharts.AnimationOptionsObject} [animation] * If or how to apply animation to the redraw. * * @return {void} * * @fires Highcharts.Chart#event:afterSetExtremes * @fires Highcharts.Chart#event:beforeRedraw * @fires Highcharts.Chart#event:predraw * @fires Highcharts.Chart#event:redraw * @fires Highcharts.Chart#event:render * @fires Highcharts.Chart#event:updatedData */ redraw: function (animation) { fireEvent(this, 'beforeRedraw'); var chart = this, axes = chart.axes, series = chart.series, pointer = chart.pointer, legend = chart.legend, legendUserOptions = chart.userOptions.legend, redrawLegend = chart.isDirtyLegend, hasStackedSeries, hasDirtyStacks, hasCartesianSeries = chart.hasCartesianSeries, isDirtyBox = chart.isDirtyBox, i, serie, renderer = chart.renderer, isHiddenChart = renderer.isHidden(), afterRedraw = []; // Handle responsive rules, not only on resize (#6130) if (chart.setResponsive) { chart.setResponsive(false); } // Set the global animation. When chart.hasRendered is not true, the // redraw call comes from a responsive rule and animation should not // occur. setAnimation(chart.hasRendered ? animation : false, chart); if (isHiddenChart) { chart.temporaryDisplay(); } // Adjust title layout (reflow multiline text) chart.layOutTitles(); // link stacked series i = series.length; while (i--) { serie = series[i]; if (serie.options.stacking) { hasStackedSeries = true; if (serie.isDirty) { hasDirtyStacks = true; break; } } } if (hasDirtyStacks) { // mark others as dirty i = series.length; while (i--) { serie = series[i]; if (serie.options.stacking) { serie.isDirty = true; } } } // Handle updated data in the series series.forEach(function (serie) { if (serie.isDirty) { if (serie.options.legendType === 'point') { if (serie.updateTotals) { serie.updateTotals(); } redrawLegend = true; } else if (legendUserOptions && (legendUserOptions.labelFormatter || legendUserOptions.labelFormat)) { redrawLegend = true; // #2165 } } if (serie.isDirtyData) { fireEvent(serie, 'updatedData'); } }); // handle added or removed series if (redrawLegend && legend && legend.options.enabled) { // draw legend graphics legend.render(); chart.isDirtyLegend = false; } // reset stacks if (hasStackedSeries) { chart.getStacks(); } if (hasCartesianSeries) { // set axes scales axes.forEach(function (axis) { axis.updateNames(); axis.setScale(); }); } chart.getMargins(); // #3098 if (hasCartesianSeries) { // If one axis is dirty, all axes must be redrawn (#792, #2169) axes.forEach(function (axis) { if (axis.isDirty) { isDirtyBox = true; } }); // redraw axes axes.forEach(function (axis) { // Fire 'afterSetExtremes' only if extremes are set var key = axis.min + ',' + axis.max; if (axis.extKey !== key) { // #821, #4452 axis.extKey = key; // prevent a recursive call to chart.redraw() (#1119) afterRedraw.push(function () { fireEvent(axis, 'afterSetExtremes', extend(axis.eventArgs, axis.getExtremes())); // #747, #751 delete axis.eventArgs; }); } if (isDirtyBox || hasStackedSeries) { axis.redraw(); } }); } // the plot areas size has changed if (isDirtyBox) { chart.drawChartBox(); } // Fire an event before redrawing series, used by the boost module to // clear previous series renderings. fireEvent(chart, 'predraw'); // redraw affected series series.forEach(function (serie) { if ((isDirtyBox || serie.isDirty) && serie.visible) { serie.redraw(); } // Set it here, otherwise we will have unlimited 'updatedData' calls // for a hidden series after setData(). Fixes #6012 serie.isDirtyData = false; }); // move tooltip or reset if (pointer) { pointer.reset(true); } // redraw if canvas renderer.draw(); // Fire the events fireEvent(chart, 'redraw'); fireEvent(chart, 'render'); if (isHiddenChart) { chart.temporaryDisplay(true); } // Fire callbacks that are put on hold until after the redraw afterRedraw.forEach(function (callback) { callback.call(); }); }, /** * Get an axis, series or point object by `id` as given in the configuration * options. Returns `undefined` if no item is found. * * @sample highcharts/plotoptions/series-id/ * Get series by id * * @function Highcharts.Chart#get * * @param {string} id * The id as given in the configuration options. * * @return {Highcharts.Axis|Highcharts.Series|Highcharts.Point|undefined} * The retrieved item. */ get: function (id) { var ret, series = this.series, i; /** * @private * @param {Highcharts.Axis|Highcharts.Series} item * @return {boolean} */ function itemById(item) { return (item.id === id || (item.options && item.options.id === id)); } ret = // Search axes find(this.axes, itemById) || // Search series find(this.series, itemById); // Search points for (i = 0; !ret && i < series.length; i++) { ret = find(series[i].points || [], itemById); } return ret; }, /** * Create the Axis instances based on the config options. * * @private * @function Highcharts.Chart#getAxes * * @return {void} * * @fires Highcharts.Chart#event:afterGetAxes * @fires Highcharts.Chart#event:getAxes */ getAxes: function () { var chart = this, options = this.options, xAxisOptions = options.xAxis = splat(options.xAxis || {}), yAxisOptions = options.yAxis = splat(options.yAxis || {}), optionsArray; fireEvent(this, 'getAxes'); // make sure the options are arrays and add some members xAxisOptions.forEach(function (axis, i) { axis.index = i; axis.isX = true; }); yAxisOptions.forEach(function (axis, i) { axis.index = i; }); // concatenate all axis options into one array optionsArray = xAxisOptions.concat(yAxisOptions); optionsArray.forEach(function (axisOptions) { new Axis(chart, axisOptions); // eslint-disable-line no-new }); fireEvent(this, 'afterGetAxes'); }, /** * Returns an array of all currently selected points in the chart. Points * can be selected by clicking or programmatically by the * {@link Highcharts.Point#select} * function. * * @sample highcharts/plotoptions/series-allowpointselect-line/ * Get selected points * * @function Highcharts.Chart#getSelectedPoints * * @return {Array} * The currently selected points. */ getSelectedPoints: function () { var points = []; this.series.forEach(function (serie) { // For one-to-one points inspect series.data in order to retrieve // points outside the visible range (#6445). For grouped data, // inspect the generated series.points. points = points.concat(serie.getPointsCollection().filter(function (point) { return pick(point.selectedStaging, point.selected); })); }); return points; }, /** * Returns an array of all currently selected series in the chart. Series * can be selected either programmatically by the * {@link Highcharts.Series#select} * function or by checking the checkbox next to the legend item if * [series.showCheckBox](https://api.highcharts.com/highcharts/plotOptions.series.showCheckbox) * is true. * * @sample highcharts/members/chart-getselectedseries/ * Get selected series * * @function Highcharts.Chart#getSelectedSeries * * @return {Array} * The currently selected series. */ getSelectedSeries: function () { return this.series.filter(function (serie) { return serie.selected; }); }, /** * Set a new title or subtitle for the chart. * * @sample highcharts/members/chart-settitle/ * Set title text and styles * * @function Highcharts.Chart#setTitle * * @param {Highcharts.TitleOptions} [titleOptions] * New title options. The title text itself is set by the * `titleOptions.text` property. * * @param {Highcharts.SubtitleOptions} [subtitleOptions] * New subtitle options. The subtitle text itself is set by the * `subtitleOptions.text` property. * * @param {boolean} [redraw] * Whether to redraw the chart or wait for a later call to * `chart.redraw()`. * * @return {void} */ setTitle: function (titleOptions, subtitleOptions, redraw) { this.applyDescription('title', titleOptions); this.applyDescription('subtitle', subtitleOptions); // The initial call also adds the caption. On update, chart.update will // relay to Chart.setCaption. this.applyDescription('caption', void 0); this.layOutTitles(redraw); }, /** * Apply a title, subtitle or caption for the chart * * @private * @function Highcharts.Chart#applyDescription * * @param name {string} * Either title, subtitle or caption * @param {Highcharts.TitleOptions|Highcharts.SubtitleOptions|Highcharts.CaptionOptions|undefined} explicitOptions * The options to set, will be merged with default options. * * @return {void} */ applyDescription: function (name, explicitOptions) { var chart = this; // Default style var style = name === 'title' ? { color: '#333333', fontSize: this.options.isStock ? '16px' : '18px' // #2944 } : { color: '#666666' }; // Merge default options with explicit options var options = this.options[name] = merge( // Default styles (!this.styledMode && { style: style }), this.options[name], explicitOptions); var elem = this[name]; if (elem && explicitOptions) { this[name] = elem = elem.destroy(); // remove old } if (options && !elem) { elem = this.renderer.text(options.text, 0, 0, options.useHTML) .attr({ align: options.align, 'class': 'highcharts-' + name, zIndex: options.zIndex || 4 }) .add(); // Update methods, shortcut to Chart.setTitle, Chart.setSubtitle and // Chart.setCaption elem.update = function (updateOptions) { var fn = { title: 'setTitle', subtitle: 'setSubtitle', caption: 'setCaption' }[name]; chart[fn](updateOptions); }; // Presentational if (!this.styledMode) { elem.css(options.style); } /** * The chart title. The title has an `update` method that allows * modifying the options directly or indirectly via * `chart.update`. * * @sample highcharts/members/title-update/ * Updating titles * * @name Highcharts.Chart#title * @type {Highcharts.TitleObject} */ /** * The chart subtitle. The subtitle has an `update` method that * allows modifying the options directly or indirectly via * `chart.update`. * * @name Highcharts.Chart#subtitle * @type {Highcharts.SubtitleObject} */ this[name] = elem; } }, /** * Internal function to lay out the chart title, subtitle and caption, and * cache the full offset height for use in `getMargins`. The result is * stored in `this.titleOffset`. * * @private * @function Highcharts.Chart#layOutTitles * * @param {boolean} [redraw=true] * * @return {void} * * @fires Highcharts.Chart#event:afterLayOutTitles */ layOutTitles: function (redraw) { var titleOffset = [0, 0, 0], requiresDirtyBox, renderer = this.renderer, spacingBox = this.spacingBox; // Lay out the title and the subtitle respectively ['title', 'subtitle', 'caption'].forEach(function (key) { var title = this[key], titleOptions = this.options[key], verticalAlign = titleOptions.verticalAlign || 'top', offset = key === 'title' ? -3 : // Floating subtitle (#6574) verticalAlign === 'top' ? titleOffset[0] + 2 : 0, titleSize, height; if (title) { if (!this.styledMode) { titleSize = titleOptions.style.fontSize; } titleSize = renderer.fontMetrics(titleSize, title).b; title .css({ width: (titleOptions.width || spacingBox.width + (titleOptions.widthAdjust || 0)) + 'px' }); // Skip the cache for HTML (#3481, #11666) height = Math.round(title.getBBox(titleOptions.useHTML).height); title.align(extend({ y: verticalAlign === 'bottom' ? titleSize : offset + titleSize, height: height }, titleOptions), false, 'spacingBox'); if (!titleOptions.floating) { if (verticalAlign === 'top') { titleOffset[0] = Math.ceil(titleOffset[0] + height); } else if (verticalAlign === 'bottom') { titleOffset[2] = Math.ceil(titleOffset[2] + height); } } } }, this); // Handle title.margin and caption.margin if (titleOffset[0] && (this.options.title.verticalAlign || 'top') === 'top') { titleOffset[0] += this.options.title.margin; } if (titleOffset[2] && this.options.caption.verticalAlign === 'bottom') { titleOffset[2] += this.options.caption.margin; } requiresDirtyBox = (!this.titleOffset || this.titleOffset.join(',') !== titleOffset.join(',')); // Used in getMargins this.titleOffset = titleOffset; fireEvent(this, 'afterLayOutTitles'); if (!this.isDirtyBox && requiresDirtyBox) { this.isDirtyBox = this.isDirtyLegend = requiresDirtyBox; // Redraw if necessary (#2719, #2744) if (this.hasRendered && pick(redraw, true) && this.isDirtyBox) { this.redraw(); } } }, /** * Internal function to get the chart width and height according to options * and container size. Sets {@link Chart.chartWidth} and * {@link Chart.chartHeight}. * * @private * @function Highcharts.Chart#getChartSize * * @return {void} */ getChartSize: function () { var chart = this, optionsChart = chart.options.chart, widthOption = optionsChart.width, heightOption = optionsChart.height, renderTo = chart.renderTo; // Get inner width and height if (!defined(widthOption)) { chart.containerWidth = getStyle(renderTo, 'width'); } if (!defined(heightOption)) { chart.containerHeight = getStyle(renderTo, 'height'); } /** * The current pixel width of the chart. * * @name Highcharts.Chart#chartWidth * @type {number} */ chart.chartWidth = Math.max(// #1393 0, widthOption || chart.containerWidth || 600 // #1460 ); /** * The current pixel height of the chart. * * @name Highcharts.Chart#chartHeight * @type {number} */ chart.chartHeight = Math.max(0, relativeLength(heightOption, chart.chartWidth) || (chart.containerHeight > 1 ? chart.containerHeight : 400)); }, /** * If the renderTo element has no offsetWidth, most likely one or more of * its parents are hidden. Loop up the DOM tree to temporarily display the * parents, then save the original display properties, and when the true * size is retrieved, reset them. Used on first render and on redraws. * * @private * @function Highcharts.Chart#temporaryDisplay * * @param {boolean} [revert] * Revert to the saved original styles. * * @return {void} */ temporaryDisplay: function (revert) { var node = this.renderTo, tempStyle; if (!revert) { while (node && node.style) { // When rendering to a detached node, it needs to be temporarily // attached in order to read styling and bounding boxes (#5783, // #7024). if (!doc.body.contains(node) && !node.parentNode) { node.hcOrigDetached = true; doc.body.appendChild(node); } if (getStyle(node, 'display', false) === 'none' || node.hcOricDetached) { node.hcOrigStyle = { display: node.style.display, height: node.style.height, overflow: node.style.overflow }; tempStyle = { display: 'block', overflow: 'hidden' }; if (node !== this.renderTo) { tempStyle.height = 0; } css(node, tempStyle); // If it still doesn't have an offset width after setting // display to block, it probably has an !important priority // #2631, 6803 if (!node.offsetWidth) { node.style.setProperty('display', 'block', 'important'); } } node = node.parentNode; if (node === doc.body) { break; } } } else { while (node && node.style) { if (node.hcOrigStyle) { css(node, node.hcOrigStyle); delete node.hcOrigStyle; } if (node.hcOrigDetached) { doc.body.removeChild(node); node.hcOrigDetached = false; } node = node.parentNode; } } }, /** * Set the {@link Chart.container|chart container's} class name, in * addition to `highcharts-container`. * * @function Highcharts.Chart#setClassName * * @param {string} [className] * * @return {void} */ setClassName: function (className) { this.container.className = 'highcharts-container ' + (className || ''); }, /** * Get the containing element, determine the size and create the inner * container div to hold the chart. * * @private * @function Highcharts.Chart#afterGetContainer * * @return {void} * * @fires Highcharts.Chart#event:afterGetContainer */ getContainer: function () { var chart = this, container, options = chart.options, optionsChart = options.chart, chartWidth, chartHeight, renderTo = chart.renderTo, indexAttrName = 'data-highcharts-chart', oldChartIndex, Ren, containerId = uniqueKey(), containerStyle, key; if (!renderTo) { chart.renderTo = renderTo = optionsChart.renderTo; } if (isString(renderTo)) { chart.renderTo = renderTo = doc.getElementById(renderTo); } // Display an error if the renderTo is wrong if (!renderTo) { error(13, true, chart); } // If the container already holds a chart, destroy it. The check for // hasRendered is there because web pages that are saved to disk from // the browser, will preserve the data-highcharts-chart attribute and // the SVG contents, but not an interactive chart. So in this case, // charts[oldChartIndex] will point to the wrong chart if any (#2609). oldChartIndex = pInt(attr(renderTo, indexAttrName)); if (isNumber(oldChartIndex) && charts[oldChartIndex] && charts[oldChartIndex].hasRendered) { charts[oldChartIndex].destroy(); } // Make a reference to the chart from the div attr(renderTo, indexAttrName, chart.index); // remove previous chart renderTo.innerHTML = ''; // If the container doesn't have an offsetWidth, it has or is a child of // a node that has display:none. We need to temporarily move it out to a // visible state to determine the size, else the legend and tooltips // won't render properly. The skipClone option is used in sparklines as // a micro optimization, saving about 1-2 ms each chart. if (!optionsChart.skipClone && !renderTo.offsetWidth) { chart.temporaryDisplay(); } // get the width and height chart.getChartSize(); chartWidth = chart.chartWidth; chartHeight = chart.chartHeight; // Allow table cells and flex-boxes to shrink without the chart blocking // them out (#6427) css(renderTo, { overflow: 'hidden' }); // Create the inner container if (!chart.styledMode) { containerStyle = extend({ position: 'relative', // needed for context menu (avoidscrollbars) and content // overflow in IE overflow: 'hidden', width: chartWidth + 'px', height: chartHeight + 'px', textAlign: 'left', lineHeight: 'normal', zIndex: 0, '-webkit-tap-highlight-color': 'rgba(0,0,0,0)' }, optionsChart.style); } /** * The containing HTML element of the chart. The container is * dynamically inserted into the element given as the `renderTo` * parameter in the {@link Highcharts#chart} constructor. * * @name Highcharts.Chart#container * @type {Highcharts.HTMLDOMElement} */ container = createElement('div', { id: containerId }, containerStyle, renderTo); chart.container = container; // cache the cursor (#1650) chart._cursor = container.style.cursor; // Initialize the renderer Ren = H[optionsChart.renderer] || H.Renderer; /** * The renderer instance of the chart. Each chart instance has only one * associated renderer. * * @name Highcharts.Chart#renderer * @type {Highcharts.SVGRenderer} */ chart.renderer = new Ren(container, chartWidth, chartHeight, null, optionsChart.forExport, options.exporting && options.exporting.allowHTML, chart.styledMode); // Set the initial animation from the options setAnimation(void 0, chart); chart.setClassName(optionsChart.className); if (!chart.styledMode) { chart.renderer.setStyle(optionsChart.style); } else { // Initialize definitions for (key in options.defs) { // eslint-disable-line guard-for-in this.renderer.definition(options.defs[key]); } } // Add a reference to the charts index chart.renderer.chartIndex = chart.index; fireEvent(this, 'afterGetContainer'); }, /** * Calculate margins by rendering axis labels in a preliminary position. * Title, subtitle and legend have already been rendered at this stage, but * will be moved into their final positions. * * @private * @function Highcharts.Chart#getMargins * @param {boolean} skipAxes * @return {void} * @fires Highcharts.Chart#event:getMargins */ getMargins: function (skipAxes) { var _a = this, spacing = _a.spacing, margin = _a.margin, titleOffset = _a.titleOffset; this.resetMargins(); // Adjust for title and subtitle if (titleOffset[0] && !defined(margin[0])) { this.plotTop = Math.max(this.plotTop, titleOffset[0] + spacing[0]); } if (titleOffset[2] && !defined(margin[2])) { this.marginBottom = Math.max(this.marginBottom, titleOffset[2] + spacing[2]); } // Adjust for legend if (this.legend && this.legend.display) { this.legend.adjustMargins(margin, spacing); } fireEvent(this, 'getMargins'); if (!skipAxes) { this.getAxisMargins(); } }, /** * @private * @function Highcharts.Chart#getAxisMargins * @return {void} */ getAxisMargins: function () { var chart = this, // [top, right, bottom, left] axisOffset = chart.axisOffset = [0, 0, 0, 0], colorAxis = chart.colorAxis, margin = chart.margin, getOffset = function (axes) { axes.forEach(function (axis) { if (axis.visible) { axis.getOffset(); } }); }; // pre-render axes to get labels offset width if (chart.hasCartesianSeries) { getOffset(chart.axes); } else if (colorAxis && colorAxis.length) { getOffset(colorAxis); } // Add the axis offsets marginNames.forEach(function (m, side) { if (!defined(margin[side])) { chart[m] += axisOffset[side]; } }); chart.setChartSize(); }, /** * Reflows the chart to its container. By default, the chart reflows * automatically to its container following a `window.resize` event, as per * the [chart.reflow](https://api.highcharts.com/highcharts/chart.reflow) * option. However, there are no reliable events for div resize, so if the * container is resized without a window resize event, this must be called * explicitly. * * @sample highcharts/members/chart-reflow/ * Resize div and reflow * @sample highcharts/chart/events-container/ * Pop up and reflow * * @function Highcharts.Chart#reflow * * @param {global.Event} [e] * Event arguments. Used primarily when the function is called * internally as a response to window resize. */ reflow: function (e) { var chart = this, optionsChart = chart.options.chart, renderTo = chart.renderTo, hasUserSize = (defined(optionsChart.width) && defined(optionsChart.height)), width = optionsChart.width || getStyle(renderTo, 'width'), height = optionsChart.height || getStyle(renderTo, 'height'), target = e ? e.target : win; // Width and height checks for display:none. Target is doc in IE8 and // Opera, win in Firefox, Chrome and IE9. if (!hasUserSize && !chart.isPrinting && width && height && (target === win || target === doc)) { if (width !== chart.containerWidth || height !== chart.containerHeight) { U.clearTimeout(chart.reflowTimeout); // When called from window.resize, e is set, else it's called // directly (#2224) chart.reflowTimeout = syncTimeout(function () { // Set size, it may have been destroyed in the meantime // (#1257) if (chart.container) { chart.setSize(void 0, void 0, false); } }, e ? 100 : 0); } chart.containerWidth = width; chart.containerHeight = height; } }, /** * Toggle the event handlers necessary for auto resizing, depending on the * `chart.reflow` option. * * @private * @function Highcharts.Chart#setReflow * @param {boolean} [reflow] * @return {void} */ setReflow: function (reflow) { var chart = this; if (reflow !== false && !this.unbindReflow) { this.unbindReflow = addEvent(win, 'resize', function (e) { // a removed event listener still runs in Edge and IE if the // listener was removed while the event runs, so check if the // chart is not destroyed (#11609) if (chart.options) { chart.reflow(e); } }); addEvent(this, 'destroy', this.unbindReflow); } else if (reflow === false && this.unbindReflow) { // Unbind and unset this.unbindReflow = this.unbindReflow(); } // The following will add listeners to re-fit the chart before and after // printing (#2284). However it only works in WebKit. Should have worked // in Firefox, but not supported in IE. /* if (win.matchMedia) { win.matchMedia('print').addListener(function reflow() { chart.reflow(); }); } //*/ }, /** * Resize the chart to a given width and height. In order to set the width * only, the height argument may be skipped. To set the height only, pass * `undefined` for the width. * * @sample highcharts/members/chart-setsize-button/ * Test resizing from buttons * @sample highcharts/members/chart-setsize-jquery-resizable/ * Add a jQuery UI resizable * @sample stock/members/chart-setsize/ * Highstock with UI resizable * * @function Highcharts.Chart#setSize * * @param {number|null} [width] * The new pixel width of the chart. Since v4.2.6, the argument can * be `undefined` in order to preserve the current value (when * setting height only), or `null` to adapt to the width of the * containing element. * * @param {number|null} [height] * The new pixel height of the chart. Since v4.2.6, the argument can * be `undefined` in order to preserve the current value, or `null` * in order to adapt to the height of the containing element. * * @param {boolean|Highcharts.AnimationOptionsObject} [animation=true] * Whether and how to apply animation. * * @return {void} * * @fires Highcharts.Chart#event:endResize * @fires Highcharts.Chart#event:resize */ setSize: function (width, height, animation) { var chart = this, renderer = chart.renderer, globalAnimation; // Handle the isResizing counter chart.isResizing += 1; // set the animation for the current process setAnimation(animation, chart); globalAnimation = renderer.globalAnimation; chart.oldChartHeight = chart.chartHeight; chart.oldChartWidth = chart.chartWidth; if (typeof width !== 'undefined') { chart.options.chart.width = width; } if (typeof height !== 'undefined') { chart.options.chart.height = height; } chart.getChartSize(); // Resize the container with the global animation applied if enabled // (#2503) if (!chart.styledMode) { (globalAnimation ? animate : css)(chart.container, { width: chart.chartWidth + 'px', height: chart.chartHeight + 'px' }, globalAnimation); } chart.setChartSize(true); renderer.setSize(chart.chartWidth, chart.chartHeight, globalAnimation); // handle axes chart.axes.forEach(function (axis) { axis.isDirty = true; axis.setScale(); }); chart.isDirtyLegend = true; // force legend redraw chart.isDirtyBox = true; // force redraw of plot and chart border chart.layOutTitles(); // #2857 chart.getMargins(); chart.redraw(globalAnimation); chart.oldChartHeight = null; fireEvent(chart, 'resize'); // Fire endResize and set isResizing back. If animation is disabled, // fire without delay syncTimeout(function () { if (chart) { fireEvent(chart, 'endResize', null, function () { chart.isResizing -= 1; }); } }, animObject(globalAnimation).duration || 0); }, /** * Set the public chart properties. This is done before and after the * pre-render to determine margin sizes. * * @private * @function Highcharts.Chart#setChartSize * * @param {boolean} skipAxes * * @return {void} * * @fires Highcharts.Chart#event:afterSetChartSize */ setChartSize: function (skipAxes) { var chart = this, inverted = chart.inverted, renderer = chart.renderer, chartWidth = chart.chartWidth, chartHeight = chart.chartHeight, optionsChart = chart.options.chart, spacing = chart.spacing, clipOffset = chart.clipOffset, clipX, clipY, plotLeft, plotTop, plotWidth, plotHeight, plotBorderWidth; /** * The current left position of the plot area in pixels. * * @name Highcharts.Chart#plotLeft * @type {number} */ chart.plotLeft = plotLeft = Math.round(chart.plotLeft); /** * The current top position of the plot area in pixels. * * @name Highcharts.Chart#plotTop * @type {number} */ chart.plotTop = plotTop = Math.round(chart.plotTop); /** * The current width of the plot area in pixels. * * @name Highcharts.Chart#plotWidth * @type {number} */ chart.plotWidth = plotWidth = Math.max(0, Math.round(chartWidth - plotLeft - chart.marginRight)); /** * The current height of the plot area in pixels. * * @name Highcharts.Chart#plotHeight * @type {number} */ chart.plotHeight = plotHeight = Math.max(0, Math.round(chartHeight - plotTop - chart.marginBottom)); chart.plotSizeX = inverted ? plotHeight : plotWidth; chart.plotSizeY = inverted ? plotWidth : plotHeight; chart.plotBorderWidth = optionsChart.plotBorderWidth || 0; // Set boxes used for alignment chart.spacingBox = renderer.spacingBox = { x: spacing[3], y: spacing[0], width: chartWidth - spacing[3] - spacing[1], height: chartHeight - spacing[0] - spacing[2] }; chart.plotBox = renderer.plotBox = { x: plotLeft, y: plotTop, width: plotWidth, height: plotHeight }; plotBorderWidth = 2 * Math.floor(chart.plotBorderWidth / 2); clipX = Math.ceil(Math.max(plotBorderWidth, clipOffset[3]) / 2); clipY = Math.ceil(Math.max(plotBorderWidth, clipOffset[0]) / 2); chart.clipBox = { x: clipX, y: clipY, width: Math.floor(chart.plotSizeX - Math.max(plotBorderWidth, clipOffset[1]) / 2 - clipX), height: Math.max(0, Math.floor(chart.plotSizeY - Math.max(plotBorderWidth, clipOffset[2]) / 2 - clipY)) }; if (!skipAxes) { chart.axes.forEach(function (axis) { axis.setAxisSize(); axis.setAxisTranslation(); }); } fireEvent(chart, 'afterSetChartSize', { skipAxes: skipAxes }); }, /** * Initial margins before auto size margins are applied. * * @private * @function Highcharts.Chart#resetMargins * @return {void} */ resetMargins: function () { fireEvent(this, 'resetMargins'); var chart = this, chartOptions = chart.options.chart; // Create margin and spacing array ['margin', 'spacing'].forEach(function splashArrays(target) { var value = chartOptions[target], values = isObject(value) ? value : [value, value, value, value]; [ 'Top', 'Right', 'Bottom', 'Left' ].forEach(function (sideName, side) { chart[target][side] = pick(chartOptions[target + sideName], values[side]); }); }); // Set margin names like chart.plotTop, chart.plotLeft, // chart.marginRight, chart.marginBottom. marginNames.forEach(function (m, side) { chart[m] = pick(chart.margin[side], chart.spacing[side]); }); chart.axisOffset = [0, 0, 0, 0]; // top, right, bottom, left chart.clipOffset = [0, 0, 0, 0]; }, /** * Internal function to draw or redraw the borders and backgrounds for chart * and plot area. * * @private * @function Highcharts.Chart#drawChartBox * @return {void} * @fires Highcharts.Chart#event:afterDrawChartBox */ drawChartBox: function () { var chart = this, optionsChart = chart.options.chart, renderer = chart.renderer, chartWidth = chart.chartWidth, chartHeight = chart.chartHeight, chartBackground = chart.chartBackground, plotBackground = chart.plotBackground, plotBorder = chart.plotBorder, chartBorderWidth, styledMode = chart.styledMode, plotBGImage = chart.plotBGImage, chartBackgroundColor = optionsChart.backgroundColor, plotBackgroundColor = optionsChart.plotBackgroundColor, plotBackgroundImage = optionsChart.plotBackgroundImage, mgn, bgAttr, plotLeft = chart.plotLeft, plotTop = chart.plotTop, plotWidth = chart.plotWidth, plotHeight = chart.plotHeight, plotBox = chart.plotBox, clipRect = chart.clipRect, clipBox = chart.clipBox, verb = 'animate'; // Chart area if (!chartBackground) { chart.chartBackground = chartBackground = renderer.rect() .addClass('highcharts-background') .add(); verb = 'attr'; } if (!styledMode) { // Presentational chartBorderWidth = optionsChart.borderWidth || 0; mgn = chartBorderWidth + (optionsChart.shadow ? 8 : 0); bgAttr = { fill: chartBackgroundColor || 'none' }; if (chartBorderWidth || chartBackground['stroke-width']) { // #980 bgAttr.stroke = optionsChart.borderColor; bgAttr['stroke-width'] = chartBorderWidth; } chartBackground .attr(bgAttr) .shadow(optionsChart.shadow); } else { chartBorderWidth = mgn = chartBackground.strokeWidth(); } chartBackground[verb]({ x: mgn / 2, y: mgn / 2, width: chartWidth - mgn - chartBorderWidth % 2, height: chartHeight - mgn - chartBorderWidth % 2, r: optionsChart.borderRadius }); // Plot background verb = 'animate'; if (!plotBackground) { verb = 'attr'; chart.plotBackground = plotBackground = renderer.rect() .addClass('highcharts-plot-background') .add(); } plotBackground[verb](plotBox); if (!styledMode) { // Presentational attributes for the background plotBackground .attr({ fill: plotBackgroundColor || 'none' }) .shadow(optionsChart.plotShadow); // Create the background image if (plotBackgroundImage) { if (!plotBGImage) { chart.plotBGImage = renderer.image(plotBackgroundImage, plotLeft, plotTop, plotWidth, plotHeight).add(); } else { if (plotBackgroundImage !== plotBGImage.attr('href')) { plotBGImage.attr('href', plotBackgroundImage); } plotBGImage.animate(plotBox); } } } // Plot clip if (!clipRect) { chart.clipRect = renderer.clipRect(clipBox); } else { clipRect.animate({ width: clipBox.width, height: clipBox.height }); } // Plot area border verb = 'animate'; if (!plotBorder) { verb = 'attr'; chart.plotBorder = plotBorder = renderer.rect() .addClass('highcharts-plot-border') .attr({ zIndex: 1 // Above the grid }) .add(); } if (!styledMode) { // Presentational plotBorder.attr({ stroke: optionsChart.plotBorderColor, 'stroke-width': optionsChart.plotBorderWidth || 0, fill: 'none' }); } plotBorder[verb](plotBorder.crisp({ x: plotLeft, y: plotTop, width: plotWidth, height: plotHeight }, -plotBorder.strokeWidth())); // #3282 plotBorder should be negative; // reset chart.isDirtyBox = false; fireEvent(this, 'afterDrawChartBox'); }, /** * Detect whether a certain chart property is needed based on inspecting its * options and series. This mainly applies to the chart.inverted property, * and in extensions to the chart.angular and chart.polar properties. * * @private * @function Highcharts.Chart#propFromSeries * @return {void} */ propFromSeries: function () { var chart = this, optionsChart = chart.options.chart, klass, seriesOptions = chart.options.series, i, value; /** * The flag is set to `true` if a series of the chart is inverted. * * @name Highcharts.Chart#inverted * @type {boolean|undefined} */ ['inverted', 'angular', 'polar'].forEach(function (key) { // The default series type's class klass = seriesTypes[(optionsChart.type || optionsChart.defaultSeriesType)]; // Get the value from available chart-wide properties value = // It is set in the options: optionsChart[key] || // The default series class: (klass && klass.prototype[key]); // requires it // 4. Check if any the chart's series require it i = seriesOptions && seriesOptions.length; while (!value && i--) { klass = seriesTypes[seriesOptions[i].type]; if (klass && klass.prototype[key]) { value = true; } } // Set the chart property chart[key] = value; }); }, /** * Internal function to link two or more series together, based on the * `linkedTo` option. This is done from `Chart.render`, and after * `Chart.addSeries` and `Series.remove`. * * @private * @function Highcharts.Chart#linkSeries * @return {void} * @fires Highcharts.Chart#event:afterLinkSeries */ linkSeries: function () { var chart = this, chartSeries = chart.series; // Reset links chartSeries.forEach(function (series) { series.linkedSeries.length = 0; }); // Apply new links chartSeries.forEach(function (series) { var linkedTo = series.options.linkedTo; if (isString(linkedTo)) { if (linkedTo === ':previous') { linkedTo = chart.series[series.index - 1]; } else { linkedTo = chart.get(linkedTo); } // #3341 avoid mutual linking if (linkedTo && linkedTo.linkedParent !== series) { linkedTo.linkedSeries.push(series); series.linkedParent = linkedTo; if (linkedTo.enabledDataSorting) { series.setDataSortingOptions(); } series.visible = pick(series.options.visible, linkedTo.options.visible, series.visible); // #3879 } } }); fireEvent(this, 'afterLinkSeries'); }, /** * Render series for the chart. * * @private * @function Highcharts.Chart#renderSeries * @return {void} */ renderSeries: function () { this.series.forEach(function (serie) { serie.translate(); serie.render(); }); }, /** * Render labels for the chart. * * @private * @function Highcharts.Chart#renderLabels * @return {void} */ renderLabels: function () { var chart = this, labels = chart.options.labels; if (labels.items) { labels.items.forEach(function (label) { var style = extend(labels.style, label.style), x = pInt(style.left) + chart.plotLeft, y = pInt(style.top) + chart.plotTop + 12; // delete to prevent rewriting in IE delete style.left; delete style.top; chart.renderer.text(label.html, x, y) .attr({ zIndex: 2 }) .css(style) .add(); }); } }, /** * Render all graphics for the chart. Runs internally on initialization. * * @private * @function Highcharts.Chart#render * @return {void} */ render: function () { var chart = this, axes = chart.axes, colorAxis = chart.colorAxis, renderer = chart.renderer, options = chart.options, correction = 0, // correction for X axis labels tempWidth, tempHeight, redoHorizontal, redoVertical, renderAxes = function (axes) { axes.forEach(function (axis) { if (axis.visible) { axis.render(); } }); }; // Title chart.setTitle(); /** * The overview of the chart's series. * * @name Highcharts.Chart#legend * @type {Highcharts.Legend} */ chart.legend = new Legend(chart, options.legend); // Get stacks if (chart.getStacks) { chart.getStacks(); } // Get chart margins chart.getMargins(true); chart.setChartSize(); // Record preliminary dimensions for later comparison tempWidth = chart.plotWidth; axes.some(function (axis) { if (axis.horiz && axis.visible && axis.options.labels.enabled && axis.series.length) { // 21 is the most common correction for X axis labels correction = 21; return true; } }); // use Math.max to prevent negative plotHeight chart.plotHeight = Math.max(chart.plotHeight - correction, 0); tempHeight = chart.plotHeight; // Get margins by pre-rendering axes axes.forEach(function (axis) { axis.setScale(); }); chart.getAxisMargins(); // If the plot area size has changed significantly, calculate tick // positions again redoHorizontal = tempWidth / chart.plotWidth > 1.1; // Height is more sensitive, use lower threshold redoVertical = tempHeight / chart.plotHeight > 1.05; if (redoHorizontal || redoVertical) { axes.forEach(function (axis) { if ((axis.horiz && redoHorizontal) || (!axis.horiz && redoVertical)) { // update to reflect the new margins axis.setTickInterval(true); } }); chart.getMargins(); // second pass to check for new labels } // Draw the borders and backgrounds chart.drawChartBox(); // Axes if (chart.hasCartesianSeries) { renderAxes(axes); } else if (colorAxis && colorAxis.length) { renderAxes(colorAxis); } // The series if (!chart.seriesGroup) { chart.seriesGroup = renderer.g('series-group') .attr({ zIndex: 3 }) .add(); } chart.renderSeries(); // Labels chart.renderLabels(); // Credits chart.addCredits(); // Handle responsiveness if (chart.setResponsive) { chart.setResponsive(); } // Handle scaling chart.updateContainerScaling(); // Set flag chart.hasRendered = true; }, /** * Set a new credits label for the chart. * * @sample highcharts/credits/credits-update/ * Add and update credits * * @function Highcharts.Chart#addCredits * * @param {Highcharts.CreditsOptions} credits * A configuration object for the new credits. * * @return {void} */ addCredits: function (credits) { var chart = this; credits = merge(true, this.options.credits, credits); if (credits.enabled && !this.credits) { /** * The chart's credits label. The label has an `update` method that * allows setting new options as per the * [credits options set](https://api.highcharts.com/highcharts/credits). * * @name Highcharts.Chart#credits * @type {Highcharts.SVGElement} */ this.credits = this.renderer.text(credits.text + (this.mapCredits || ''), 0, 0) .addClass('highcharts-credits') .on('click', function () { if (credits.href) { win.location.href = credits.href; } }) .attr({ align: credits.position.align, zIndex: 8 }); if (!chart.styledMode) { this.credits.css(credits.style); } this.credits .add() .align(credits.position); // Dynamically update this.credits.update = function (options) { chart.credits = chart.credits.destroy(); chart.addCredits(options); }; } }, /** * Handle scaling, #11329 - when there is scaling/transform on the container * or on a parent element, we need to take this into account. We calculate * the scaling once here and it is picked up where we need to use it * (Pointer, Tooltip). * * @private * @function Highcharts.Chart#updateContainerScaling * @return {void} */ updateContainerScaling: function () { var container = this.container; if (container.offsetWidth && container.offsetHeight && container.getBoundingClientRect) { var bb = container.getBoundingClientRect(), scaleX = bb.width / container.offsetWidth, scaleY = bb.height / container.offsetHeight; if (scaleX !== 1 || scaleY !== 1) { this.containerScaling = { scaleX: scaleX, scaleY: scaleY }; } else { delete this.containerScaling; } } }, /** * Remove the chart and purge memory. This method is called internally * before adding a second chart into the same container, as well as on * window unload to prevent leaks. * * @sample highcharts/members/chart-destroy/ * Destroy the chart from a button * @sample stock/members/chart-destroy/ * Destroy with Highstock * * @function Highcharts.Chart#destroy * * @return {void} * * @fires Highcharts.Chart#event:destroy */ destroy: function () { var chart = this, axes = chart.axes, series = chart.series, container = chart.container, i, parentNode = container && container.parentNode; // fire the chart.destoy event fireEvent(chart, 'destroy'); // Delete the chart from charts lookup array if (chart.renderer.forExport) { erase(charts, chart); // #6569 } else { charts[chart.index] = void 0; } H.chartCount--; chart.renderTo.removeAttribute('data-highcharts-chart'); // remove events removeEvent(chart); // ==== Destroy collections: // Destroy axes i = axes.length; while (i--) { axes[i] = axes[i].destroy(); } // Destroy scroller & scroller series before destroying base series if (this.scroller && this.scroller.destroy) { this.scroller.destroy(); } // Destroy each series i = series.length; while (i--) { series[i] = series[i].destroy(); } // ==== Destroy chart properties: [ 'title', 'subtitle', 'chartBackground', 'plotBackground', 'plotBGImage', 'plotBorder', 'seriesGroup', 'clipRect', 'credits', 'pointer', 'rangeSelector', 'legend', 'resetZoomButton', 'tooltip', 'renderer' ].forEach(function (name) { var prop = chart[name]; if (prop && prop.destroy) { chart[name] = prop.destroy(); } }); // Remove container and all SVG, check container as it can break in IE // when destroyed before finished loading if (container) { container.innerHTML = ''; removeEvent(container); if (parentNode) { discardElement(container); } } // clean it all up objectEach(chart, function (val, key) { delete chart[key]; }); }, /** * Prepare for first rendering after all data are loaded. * * @private * @function Highcharts.Chart#firstRender * @return {void} * * @fires Highcharts.Chart#event:beforeRender */ firstRender: function () { var chart = this, options = chart.options; // Hook for oldIE to check whether the chart is ready to render if (chart.isReadyToRender && !chart.isReadyToRender()) { return; } // Create the container chart.getContainer(); chart.resetMargins(); chart.setChartSize(); // Set the common chart properties (mainly invert) from the given series chart.propFromSeries(); // get axes chart.getAxes(); // Initialize the series (isArray(options.series) ? options.series : []).forEach( // #9680 function (serieOptions) { chart.initSeries(serieOptions); }); chart.linkSeries(); chart.setSeriesData(); // Run an event after axes and series are initialized, but before // render. At this stage, the series data is indexed and cached in the // xData and yData arrays, so we can access those before rendering. Used // in Highstock. fireEvent(chart, 'beforeRender'); // depends on inverted and on margins being set if (Pointer) { if (!H.hasTouch && (win.PointerEvent || win.MSPointerEvent)) { chart.pointer = new MSPointer(chart, options); } else { /** * The Pointer that keeps track of mouse and touch interaction. * * @memberof Highcharts.Chart * @name pointer * @type {Highcharts.Pointer} * @instance */ chart.pointer = new Pointer(chart, options); } } chart.render(); // Fire the load event if there are no external images if (!chart.renderer.imgCount && !chart.hasLoaded) { chart.onload(); } // If the chart was rendered outside the top container, put it back in // (#3679) chart.temporaryDisplay(true); }, /** * Internal function that runs on chart load, async if any images are loaded * in the chart. Runs the callbacks and triggers the `load` and `render` * events. * * @private * @function Highcharts.Chart#onload * @return {void} * @fires Highcharts.Chart#event:load * @fires Highcharts.Chart#event:render */ onload: function () { // Run callbacks, first the ones registered by modules, then user's one this.callbacks.concat([this.callback]).forEach(function (fn) { // Chart destroyed in its own callback (#3600) if (fn && typeof this.index !== 'undefined') { fn.apply(this, [this]); } }, this); fireEvent(this, 'load'); fireEvent(this, 'render'); // Set up auto resize, check for not destroyed (#6068) if (defined(this.index)) { this.setReflow(this.options.chart.reflow); } // Don't run again this.hasLoaded = true; } }); // end Chart