UNPKG

highcharts

Version:
1,270 lines 118 kB
/* * * * (c) 2010-2024 Torstein Honsi * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ 'use strict'; import A from '../Animation/AnimationUtilities.js'; const { animate, animObject, setAnimation } = A; import Axis from '../Axis/Axis.js'; import D from '../Defaults.js'; const { defaultOptions } = D; import Templating from '../Templating.js'; const { numberFormat } = Templating; import Foundation from '../Foundation.js'; const { registerEventOptions } = Foundation; import H from '../Globals.js'; const { charts, doc, marginNames, svg, win } = H; import RendererRegistry from '../Renderer/RendererRegistry.js'; import Series from '../Series/Series.js'; import SeriesRegistry from '../Series/SeriesRegistry.js'; const { seriesTypes } = SeriesRegistry; import SVGRenderer from '../Renderer/SVG/SVGRenderer.js'; import Time from '../Time.js'; import U from '../Utilities.js'; import AST from '../Renderer/HTML/AST.js'; import Tick from '../Axis/Tick.js'; const { addEvent, attr, createElement, css, defined, diffObjects, discardElement, erase, error, extend, find, fireEvent, getAlignFactor, getStyle, isArray, isNumber, isObject, isString, merge, objectEach, pick, pInt, relativeLength, removeEvent, splat, syncTimeout, uniqueKey } = U; /* * * * Class * * */ /* eslint-disable no-invalid-this, valid-jsdoc */ /** * The Chart class. The recommended constructor is {@link Highcharts#chart}. * * @example * let 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 all external images * are loaded. Defining a * [chart.events.load](https://api.highcharts.com/highcharts/chart.events.load) * handler is equivalent. */ class Chart { /** * Factory function for basic charts. * * @example * // Render a chart in to div#container * let 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 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. */ static chart(a, b, c) { return new Chart(a, b, c); } // Implementation constructor(a, /* eslint-disable @typescript-eslint/no-unused-vars */ b, c /* eslint-enable @typescript-eslint/no-unused-vars */ ) { this.sharedClips = {}; const args = [ // ES5 builds fail unless we cast it to an Array ...arguments ]; // Remove the optional first argument, renderTo, and set it on this. if (isString(a) || a.nodeName) { this.renderTo = args.shift(); } this.init(args[0], args[1]); } /* * * * Functions * * */ /** * Function setting zoom options after chart init and after chart update. * Offers support for deprecated options. * * @private * @function Highcharts.Chart#setZoomOptions */ setZoomOptions() { const chart = this, options = chart.options.chart, zooming = options.zooming; chart.zooming = { ...zooming, type: pick(options.zoomType, zooming.type), key: pick(options.zoomKey, zooming.key), pinchType: pick(options.pinchType, zooming.pinchType), singleTouch: pick(options.zoomBySingleTouch, zooming.singleTouch, false), resetButton: merge(zooming.resetButton, options.resetZoomButton) }; } /** * 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 all external * images are loaded. * * * @emits Highcharts.Chart#event:init * @emits Highcharts.Chart#event:afterInit */ init(userOptions, callback) { // Fire the event with a default function fireEvent(this, 'init', { args: arguments }, function () { const options = merge(defaultOptions, userOptions), // Do the merge optionsChart = options.chart, renderTo = this.renderTo || optionsChart.renderTo; /** * The original options given to the constructor or a chart factory * like {@link Highcharts.chart} and {@link Highcharts.stockChart}. * The original options are shallow copied to avoid mutation. The * copy, `chart.userOptions`, may later be mutated to reflect * updated options throughout the lifetime of the chart. * * For collections, like `series`, `xAxis` and `yAxis`, the chart * user options should always be reflected by the item user option, * so for example the following should always be true: * * `chart.xAxis[0].userOptions === chart.userOptions.xAxis[0]` * * @name Highcharts.Chart#userOptions * @type {Highcharts.Options} */ this.userOptions = extend({}, userOptions); if (!(this.renderTo = (isString(renderTo) ? doc.getElementById(renderTo) : renderTo))) { // Display an error if the renderTo is wrong error(13, true, this); } this.margin = []; this.spacing = []; // 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 = []; this.locale = options.lang.locale ?? this.renderTo.closest('[lang]')?.lang; /** * 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 = new Time(extend(options.time || {}, { locale: this.locale })); options.time = this.time.options; /** * 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).bind(this); /** * Whether the chart is in styled mode, meaning all presentational * attributes are avoided. * * @name Highcharts.Chart#styledMode * @type {boolean} */ this.styledMode = optionsChart.styledMode; this.hasCartesianSeries = optionsChart.showAxes; const 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 registerEventOptions(this, optionsChart); /** * 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; this.setZoomOptions(); // 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 */ initSeries(options) { const chart = this, optionsChart = chart.options.chart, type = (options.type || optionsChart.type), SeriesClass = seriesTypes[type]; // No such series type if (!SeriesClass) { error(17, true, chart, { missingModuleFor: type }); } const 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#setSortedData */ setSortedData() { 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 */ getSeriesOrderByLinks() { 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 or axes above a given index. When series or axes are * added and ordered by configuration, only the last series is handled * (#248, #1123, #2456, #6112). This function is called on series and axis * initialization and destroy. * * @private * @function Highcharts.Chart#orderItems * @param {string} coll The collection name * @param {number} [fromIndex=0] * If this is given, only the series above this index are handled. */ orderItems(coll, fromIndex = 0) { const collection = this[coll], // Item options should be reflected in chart.options.series, // chart.options.yAxis etc optionsArray = this.options[coll] = splat(this.options[coll]) .slice(), userOptionsArray = this.userOptions[coll] = this.userOptions[coll] ? splat(this.userOptions[coll]).slice() : []; if (this.hasRendered) { // Remove all above index optionsArray.splice(fromIndex); userOptionsArray.splice(fromIndex); } if (collection) { for (let i = fromIndex, iEnd = collection.length; i < iEnd; ++i) { const item = collection[i]; if (item) { /** * Contains the series' index in the `Chart.series` array. * * @name Highcharts.Series#index * @type {number} * @readonly */ item.index = i; if (item instanceof Series) { item.name = item.getName(); } if (!item.options.isInternal) { optionsArray[i] = item.options; userOptionsArray[i] = item.userOptions; } } } } } /** * 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 {Highcharts.ChartIsInsideOptionsObject} [options] * Options object. * * @return {boolean} * Returns true if the given point is inside the plot area. */ isInsidePlot(plotX, plotY, options = {}) { const { inverted, plotBox, plotLeft, plotTop, scrollablePlotBox } = this, { scrollLeft = 0, scrollTop = 0 } = (options.visiblePlotOnly && this.scrollablePlotArea?.scrollingContainer) || {}, series = options.series, box = (options.visiblePlotOnly && scrollablePlotBox) || plotBox, x = options.inverted ? plotY : plotX, y = options.inverted ? plotX : plotY, e = { x, y, isInsidePlot: true, options }; if (!options.ignoreX) { const xAxis = (series && (inverted && !this.polar ? series.yAxis : series.xAxis)) || { pos: plotLeft, len: Infinity }; const chartX = options.paneCoordinates ? xAxis.pos + x : plotLeft + x; if (!(chartX >= Math.max(scrollLeft + plotLeft, xAxis.pos) && chartX <= Math.min(scrollLeft + plotLeft + box.width, xAxis.pos + xAxis.len))) { e.isInsidePlot = false; } } if (!options.ignoreY && e.isInsidePlot) { const yAxis = (!inverted && options.axis && !options.axis.isXAxis && options.axis) || (series && (inverted ? series.xAxis : series.yAxis)) || { pos: plotTop, len: Infinity }; const chartY = options.paneCoordinates ? yAxis.pos + y : plotTop + y; if (!(chartY >= Math.max(scrollTop + plotTop, yAxis.pos) && chartY <= Math.min(scrollTop + plotTop + box.height, yAxis.pos + yAxis.len))) { e.isInsidePlot = false; } } 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. When `undefined`, it applies * the animation that is set in the `chart.animation` option. * * @emits Highcharts.Chart#event:afterSetExtremes * @emits Highcharts.Chart#event:beforeRedraw * @emits Highcharts.Chart#event:predraw * @emits Highcharts.Chart#event:redraw * @emits Highcharts.Chart#event:render * @emits Highcharts.Chart#event:updatedData */ redraw(animation) { fireEvent(this, 'beforeRedraw'); const chart = this, axes = chart.hasCartesianSeries ? chart.axes : chart.colorAxis || [], series = chart.series, pointer = chart.pointer, legend = chart.legend, legendUserOptions = chart.userOptions.legend, renderer = chart.renderer, isHiddenChart = renderer.isHidden(), afterRedraw = []; let hasDirtyStacks, hasStackedSeries, i, isDirtyBox = chart.isDirtyBox, redrawLegend = chart.isDirtyLegend, serie; renderer.rootFontSize = renderer.boxWrapper.getStyle('font-size'); // 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(false); // 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 const 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(id) { const series = this.series; /** * @private */ function itemById(item) { return (item.id === id || (item.options && item.options.id === id)); } let ret = // Search axes find(this.axes, itemById) || // Search series find(this.series, itemById); // Search points for (let 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#createAxes * @emits Highcharts.Chart#event:afterCreateAxes * @emits Highcharts.Chart#event:createAxes */ createAxes() { const options = this.userOptions; fireEvent(this, 'createAxes'); for (const coll of ['xAxis', 'yAxis']) { const arr = options[coll] = splat(options[coll] || {}); for (const axisOptions of arr) { // eslint-disable-next-line no-new new Axis(this, axisOptions, coll); } } fireEvent(this, 'afterCreateAxes'); } /** * 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 * @sample highcharts/members/point-select-lasso/ * Lasso selection * @sample highcharts/chart/events-selection-points/ * Rectangle selection * * @function Highcharts.Chart#getSelectedPoints * * @return {Array<Highcharts.Point>} * The currently selected points. */ getSelectedPoints() { return this.series.reduce((acc, series) => { // 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. series.getPointsCollection() .forEach((point) => { if (pick(point.selectedStaging, point.selected)) { acc.push(point); } }); return acc; }, []); } /** * 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. */ getSelectedSeries() { return this.series.filter((s) => s.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()`. */ setTitle(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 key {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. */ applyDescription(key, explicitOptions) { const chart = this; // Merge default options with explicit options const options = this.options[key] = merge(this.options[key], explicitOptions); let elem = this[key]; if (elem && explicitOptions) { this[key] = elem = elem.destroy(); // Remove old } if (options && !elem) { elem = this.renderer.text(options.text, 0, 0, options.useHTML) .attr({ align: options.align, 'class': 'highcharts-' + key, zIndex: options.zIndex || 4 }) .css({ textOverflow: 'ellipsis', whiteSpace: 'nowrap' }) .add(); // Update methods, relay to `applyDescription` elem.update = function (updateOptions, redraw) { chart.applyDescription(key, updateOptions); chart.layOutTitles(redraw); }; // Presentational if (!this.styledMode) { elem.css(extend(key === 'title' ? { // #2944 fontSize: this.options.isStock ? '1em' : '1.2em' } : {}, options.style)); } // Get unwrapped text length and reset elem.textPxLength = elem.getBBox().width; elem.css({ whiteSpace: options.style?.whiteSpace }); /** * 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[key] = 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] * @emits Highcharts.Chart#event:afterLayOutTitles */ layOutTitles(redraw = true) { const titleOffset = [0, 0, 0], { options, renderer, spacingBox } = this; // Lay out the title, subtitle and caption respectively ['title', 'subtitle', 'caption'].forEach((key) => { const desc = this[key], descOptions = this.options[key], alignTo = merge(spacingBox), textPxLength = desc?.textPxLength || 0; if (desc && descOptions) { // Provide a hook for the exporting button to shift the title fireEvent(this, 'layOutTitle', { alignTo, key, textPxLength }); const fontMetrics = renderer.fontMetrics(desc), baseline = fontMetrics.b, lineHeight = fontMetrics.h, verticalAlign = descOptions.verticalAlign || 'top', topAligned = verticalAlign === 'top', // Use minScale only for top-aligned titles. It is not // likely that we will need scaling for other positions, but // if it is requested, we need to adjust the vertical // position to the scale. minScale = topAligned && descOptions.minScale || 1, offset = key === 'title' ? topAligned ? -3 : 0 : // Floating subtitle (#6574) topAligned ? titleOffset[0] + 2 : 0, uncappedScale = Math.min(alignTo.width / textPxLength, 1), scale = Math.max(minScale, uncappedScale), alignAttr = merge({ y: verticalAlign === 'bottom' ? baseline : offset + baseline }, { align: key === 'title' ? // Title defaults to center for short titles, // left for word-wrapped titles (uncappedScale < minScale ? 'left' : 'center') : // Subtitle defaults to the title.align this.title?.alignValue }, descOptions), width = descOptions.width || ((uncappedScale > minScale ? // One line this.chartWidth : // Allow word wrap alignTo.width) / scale); // No animation when switching alignment if (desc.alignValue !== alignAttr.align) { desc.placed = false; } // Set the width and read the height const height = Math.round(desc .css({ width: `${width}px` }) // Skip the cache for HTML (#3481, #11666) .getBBox(descOptions.useHTML).height); alignAttr.height = height; // Perform scaling and alignment desc .align(alignAttr, false, alignTo) .attr({ align: alignAttr.align, scaleX: scale, scaleY: scale, 'transform-origin': `${alignTo.x + textPxLength * scale * getAlignFactor(alignAttr.align)} ${lineHeight}` }); // Adjust the rendered title offset if (!descOptions.floating) { const offset = height * ( // When scaling down the title, preserve the offset as // long as it's only one line, but scale down the offset // if the title wraps to multiple lines. height < lineHeight * 1.2 ? 1 : scale); if (verticalAlign === 'top') { titleOffset[0] = Math.ceil(titleOffset[0] + offset); } else if (verticalAlign === 'bottom') { titleOffset[2] = Math.ceil(titleOffset[2] + offset); } } } }, this); // Handle title.margin and caption.margin if (titleOffset[0] && (options.title?.verticalAlign || 'top') === 'top') { titleOffset[0] += options.title?.margin || 0; } if (titleOffset[2] && options.caption?.verticalAlign === 'bottom') { titleOffset[2] += options.caption?.margin || 0; } const 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 && redraw && this.isDirtyBox) { this.redraw(); } } } /** * Internal function to get the available size of the container element * * @private * @function Highcharts.Chart#getContainerBox */ getContainerBox() { // Temporarily hide support divs from a11y and others, #21888 const nonContainers = [].map.call(this.renderTo.children, (child) => { if (child !== this.container) { const display = child.style.display; child.style.display = 'none'; return [child, display]; } }), box = { width: getStyle(this.renderTo, 'width', true) || 0, height: (getStyle(this.renderTo, 'height', true) || 0) }; // Restore the non-containers nonContainers.filter(Boolean).forEach(([div, display]) => { div.style.display = display; }); return box; } /** * 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 */ getChartSize() { const chart = this, optionsChart = chart.options.chart, widthOption = optionsChart.width, heightOption = optionsChart.height, containerBox = chart.getContainerBox(), enableDefaultHeight = containerBox.height <= 1 || ( // #21510, prevent infinite reflow !chart.renderTo.parentElement?.style.height && chart.renderTo.style.height === '100%'); /** * The current pixel width of the chart. * * @name Highcharts.Chart#chartWidth * @type {number} */ chart.chartWidth = Math.max(// #1393 0, widthOption || containerBox.width || 600 // #1460 ); /** * The current pixel height of the chart. * * @name Highcharts.Chart#chartHeight * @type {number} */ chart.chartHeight = Math.max(0, relativeLength(heightOption, chart.chartWidth) || (enableDefaultHeight ? 400 : containerBox.height)); chart.containerBox = containerBox; } /** * 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. */ temporaryDisplay(revert) { let 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. */ setClassName(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 * @emits Highcharts.Chart#event:afterGetContainer */ getContainer() { const chart = this, options = chart.options, optionsChart = options.chart, indexAttrName = 'data-highcharts-chart', containerId = uniqueKey(), renderTo = chart.renderTo; let containerStyle; // 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). const 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 = AST.emptyHTML; // 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(); const chartHeight = chart.chartHeight; let chartWidth = chart.chartWidth; // 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', // #427 zIndex: 0, // #1072 '-webkit-tap-highlight-color': 'rgba(0,0,0,0)', userSelect: 'none', // #13503 'touch-action': 'manipulation', outline: 'none', padding: '0px' }, 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} */ const container = createElement('div', { id: containerId }, containerStyle, renderTo); chart.container = container; // Adjust width if setting height affected it (#20334) chart.getChartSize(); if (chartWidth !== chart.chartWidth) { chartWidth = chart.chartWidth; if (!chart.styledMode) { css(container, { width: pick(optionsChart.style?.width, chartWidth + 'px') }); } } chart.containerBox = chart.getContainerBox(); // Cache the cursor (#1650) chart._cursor = container.style.cursor; // Initialize the renderer const Renderer = optionsChart.renderer || !svg ? RendererRegistry.getRendererType(optionsChart.renderer) : SVGRenderer; /** * The renderer instance of the chart. Each chart instance has only one * associated renderer. * * @name Highcharts.Chart#renderer * @type {Highcharts.SVGRenderer} */ chart.renderer = new Renderer(container, chartWidth, chartHeight, void 0, 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 (const 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 * @emits Highcharts.Chart#event:getMargins */ getMargins(skipAxes) { const { spacing, margin, titleOffset } = this; 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 */ getAxisMargins() { const 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(); } /** * Return the current options of the chart, but only those that differ from * default options. Items that can be either an object or an array of * objects, like `series`, `xAxis` and `yAxis`, are always returned as * array. * * @sample highcharts/members/chart-getoptions * * @function Highcharts.Chart#getOptions * * @since 11.1.0 */ getOptions() { return diffObjects(this.userOptions, defaultOptions); } /** * Reflows the chart to its container. By default, the Resize Observer is * attached to the chart's div which allows to reflows the chart * automatically to its container, as per the * [chart.reflow](https://api.highcharts.com/highcharts/chart.reflow) * option. * * @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(e) { const chart = this, oldBox = chart.containerBox, containerBox = chart.getContainerBox(); delete chart.pointer?.chartPosition; // Width and height checks for display:none. Target is doc in Opera // and win in Firefox, Chrome and IE9. if (!chart.isPrinting && !chart.isResizing && oldBox && // When fired by resize observer inside hidden container containerBox.width) { if (containerBox.width !== oldBox.width || containerBox.height !== oldBox.height) { 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.containerBox = containerBox; } } /** * Toggle the event handlers necessary for auto resizing, depending on the * `chart.reflow` option. * * @private * @function Highcharts.Chart#setReflow */ setReflow() { const chart = this; const runReflow = (e) => { if (chart.options?.chart.reflow && chart.hasLoaded) { chart.reflow(e); } }; if (typeof ResizeObserver === 'function')