UNPKG

highcharts

Version:
1,267 lines (1,266 loc) 113 kB
/* * * * (c) 2010-2021 Torstein Honsi * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ 'use strict'; import A from '../Animation/AnimationUtilities.js'; var animate = A.animate, animObject = A.animObject, setAnimation = A.setAnimation; import Axis from '../Axis/Axis.js'; import H from '../Globals.js'; var charts = H.charts, doc = H.doc, win = H.win; import Legend from '../Legend.js'; import MSPointer from '../MSPointer.js'; import O from '../Options.js'; var defaultOptions = O.defaultOptions, time = O.time; import palette from '../../Core/Color/Palette.js'; import Pointer from '../Pointer.js'; import SeriesRegistry from '../Series/SeriesRegistry.js'; var seriesTypes = SeriesRegistry.seriesTypes; import Time from '../Time.js'; import U from '../Utilities.js'; import AST from '../Renderer/HTML/AST.js'; var addEvent = U.addEvent, attr = U.attr, cleanRecursively = U.cleanRecursively, 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, splat = U.splat, syncTimeout = U.syncTimeout, uniqueKey = U.uniqueKey; var marginNames = H.marginNames; /* 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 = /** @class */ (function () { function Chart(a, b, c) { this.axes = void 0; this.axisOffset = void 0; this.bounds = void 0; this.chartHeight = void 0; this.chartWidth = void 0; this.clipBox = void 0; this.colorCounter = void 0; this.container = void 0; this.index = void 0; this.isResizing = void 0; this.labelCollectors = void 0; this.legend = void 0; this.margin = void 0; this.numberFormatter = void 0; this.options = void 0; this.plotBox = void 0; this.plotHeight = void 0; this.plotLeft = void 0; this.plotTop = void 0; this.plotWidth = void 0; this.pointCount = void 0; this.pointer = void 0; this.renderer = void 0; this.renderTo = void 0; this.series = void 0; this.spacing = void 0; this.spacingBox = void 0; this.symbolCounter = void 0; this.time = void 0; this.titleOffset = void 0; this.userOptions = void 0; this.xAxis = void 0; this.yAxis = void 0; this.getArgs(a, b, c); } /* * * * Functions * * */ /** * Handle the arguments passed to the constructor. * * @private * @function Highcharts.Chart#getArgs * * @param {...Array<*>} arguments * All arguments for the constructor. * * @fires Highcharts.Chart#event:init * @fires Highcharts.Chart#event:afterInit */ Chart.prototype.getArgs = function (a, b, c) { // Remove the optional first argument, renderTo, and // set it on this. if (isString(a) || a.nodeName) { this.renderTo = a; this.init(b, c); } else { this.init(a, b); } }; /** * 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 */ Chart.prototype.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<Highcharts.Axis>} */ this.axes = []; /** * All the current series in the chart. * * @name Highcharts.Chart#series * @type {Array<Highcharts.Series>} */ 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<Highcharts.Axis>} */ chart.xAxis = []; /** * A collection of the Y axes in the chart. * * @name Highcharts.Chart#yAxis * @type {Array<Highcharts.Axis>} * * @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 */ Chart.prototype.initSeries = function (options) { var chart = this, optionsChart = chart.options.chart, type = (options.type || optionsChart.type || optionsChart.defaultSeriesType), series, SeriesClass = seriesTypes[type]; // No such series type if (!SeriesClass) { error(17, true, chart, { missingModuleFor: type }); } series = new SeriesClass(); if (typeof series.init === 'function') { series.init(chart, options); } return series; }; /** * Internal function to set data for all series with enabled sorting. * * @private * @function Highcharts.Chart#setSeriesData */ Chart.prototype.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<Highcharts.Series>} */ Chart.prototype.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. */ Chart.prototype.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. */ Chart.prototype.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|Partial<Highcharts.AnimationOptionsObject>} [animation] * If or how to apply animation to the redraw. * * @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 */ Chart.prototype.redraw = function (animation) { fireEvent(this, 'beforeRedraw'); var chart = this, axes = chart.hasCartesianSeries ? chart.axes : chart.colorAxis || [], series = chart.series, pointer = chart.pointer, legend = chart.legend, legendUserOptions = chart.userOptions.legend, redrawLegend = chart.isDirtyLegend, hasStackedSeries, hasDirtyStacks, 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 || serie.options.centerInCategory) { 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 (typeof serie.updateTotals === 'function') { 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(); } // set axes scales axes.forEach(function (axis) { axis.updateNames(); axis.setScale(); }); chart.getMargins(); // #3098 // 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. */ Chart.prototype.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 * @fires Highcharts.Chart#event:afterGetAxes * @fires Highcharts.Chart#event:getAxes */ Chart.prototype.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<Highcharts.Point>} * The currently selected points. */ Chart.prototype.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<Highcharts.Series>} * The currently selected series. */ Chart.prototype.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()`. */ Chart.prototype.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. */ Chart.prototype.applyDescription = function (name, explicitOptions) { var chart = this; // Default style var style = name === 'title' ? { color: palette.neutralColor80, fontSize: this.options.isStock ? '16px' : '18px' // #2944 } : { color: palette.neutralColor60 }; // 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] * @fires Highcharts.Chart#event:afterLayOutTitles */ Chart.prototype.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 */ Chart.prototype.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. */ Chart.prototype.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] * The additional class name. */ Chart.prototype.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 * @fires Highcharts.Chart#event:afterGetContainer */ Chart.prototype.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)', userSelect: 'none' // #13503 }, 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 * @fires Highcharts.Chart#event:getMargins */ Chart.prototype.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 */ Chart.prototype.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. */ Chart.prototype.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; delete chart.pointer.chartPosition; // 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 */ Chart.prototype.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|Partial<Highcharts.AnimationOptionsObject>} [animation=true] * Whether and how to apply animation. * * @return {void} * * @fires Highcharts.Chart#event:endResize * @fires Highcharts.Chart#event:resize */ Chart.prototype.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); }; /** * Set the public chart properties. This is done before and after the * pre-render to determine margin sizes. * * @private * @function Highcharts.Chart#setChartSize * @fires Highcharts.Chart#event:afterSetChartSize */