highcharts
Version:
JavaScript charting framework
1,270 lines • 118 kB
JavaScript
/* *
*
* (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')