highcharts
Version:
JavaScript charting framework
1,319 lines • 76.5 kB
JavaScript
/* *
*
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import H from './Globals.js';
/**
* Callback for chart constructors.
*
* @callback Highcharts.ChartCallbackFunction
*
* @param {Highcharts.Chart} chart
* Created chart.
*/
/**
* Format a number and return a string based on input settings.
*
* @callback Highcharts.NumberFormatterCallbackFunction
*
* @param {number} number
* The input number to format.
*
* @param {number} decimals
* The amount of decimals. A value of -1 preserves the amount in the
* input number.
*
* @param {string} [decimalPoint]
* The decimal point, defaults to the one given in the lang options, or
* a dot.
*
* @param {string} [thousandsSep]
* The thousands separator, defaults to the one given in the lang
* options, or a space character.
*
* @return {string} The formatted number.
*/
/**
* The chart title. The title has an `update` method that allows modifying the
* options directly or indirectly via `chart.update`.
*
* @interface Highcharts.TitleObject
* @extends Highcharts.SVGElement
*/ /**
* Modify options for the title.
*
* @function Highcharts.TitleObject#update
*
* @param {Highcharts.TitleOptions} titleOptions
* Options to modify.
*
* @param {boolean} [redraw=true]
* Whether to redraw the chart after the title is altered. If doing more
* operations on the chart, it is a good idea to set redraw to false and
* call {@link Chart#redraw} after.
*/
/**
* The chart subtitle. The subtitle has an `update` method that
* allows modifying the options directly or indirectly via
* `chart.update`.
*
* @interface Highcharts.SubtitleObject
* @extends Highcharts.SVGElement
*/ /**
* Modify options for the subtitle.
*
* @function Highcharts.SubtitleObject#update
*
* @param {Highcharts.SubtitleOptions} subtitleOptions
* Options to modify.
*
* @param {boolean} [redraw=true]
* Whether to redraw the chart after the subtitle is altered. If doing
* more operations on the chart, it is a good idea to set redraw to false
* and call {@link Chart#redraw} after.
*/
/**
* The chart caption. The caption has an `update` method that
* allows modifying the options directly or indirectly via
* `chart.update`.
*
* @interface Highcharts.CaptionObject
* @extends Highcharts.SVGElement
*/ /**
* Modify options for the caption.
*
* @function Highcharts.CaptionObject#update
*
* @param {Highcharts.CaptionOptions} captionOptions
* Options to modify.
*
* @param {boolean} [redraw=true]
* Whether to redraw the chart after the caption is altered. If doing
* more operations on the chart, it is a good idea to set redraw to false
* and call {@link Chart#redraw} after.
*/
import U from './Utilities.js';
var animObject = U.animObject, attr = U.attr, defined = U.defined, discardElement = U.discardElement, erase = U.erase, extend = U.extend, isArray = U.isArray, isNumber = U.isNumber, isObject = U.isObject, isString = U.isString, numberFormat = U.numberFormat, objectEach = U.objectEach, pick = U.pick, pInt = U.pInt, relativeLength = U.relativeLength, setAnimation = U.setAnimation, splat = U.splat, syncTimeout = U.syncTimeout;
import './Axis.js';
import './Legend.js';
import './Options.js';
import './Pointer.js';
var addEvent = H.addEvent, animate = H.animate, doc = H.doc, Axis = H.Axis, // @todo add as requirement
createElement = H.createElement, defaultOptions = H.defaultOptions, charts = H.charts, css = H.css, find = H.find, fireEvent = H.fireEvent, Legend = H.Legend, // @todo add as requirement
marginNames = H.marginNames, merge = H.merge, Pointer = H.Pointer, // @todo add as requirement
removeEvent = H.removeEvent, seriesTypes = H.seriesTypes, win = H.win;
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* The Chart class. The recommended constructor is {@link Highcharts#chart}.
*
* @example
* var chart = Highcharts.chart('container', {
* title: {
* text: 'My chart'
* },
* series: [{
* data: [1, 3, 2, 4]
* }]
* })
*
* @class
* @name Highcharts.Chart
*
* @param {string|Highcharts.HTMLDOMElement} [renderTo]
* The DOM element to render to, or its id.
*
* @param {Highcharts.Options} options
* The chart options structure.
*
* @param {Highcharts.ChartCallbackFunction} [callback]
* Function to run when the chart has loaded and and all external images
* are loaded. Defining a
* [chart.events.load](https://api.highcharts.com/highcharts/chart.events.load)
* handler is equivalent.
*/
var Chart = H.Chart = function () {
this.getArgs.apply(this, arguments);
};
/**
* Factory function for basic charts.
*
* @example
* // Render a chart in to div#container
* var chart = Highcharts.chart('container', {
* title: {
* text: 'My chart'
* },
* series: [{
* data: [1, 3, 2, 4]
* }]
* });
*
* @function Highcharts.chart
*
* @param {string|Highcharts.HTMLDOMElement} [renderTo]
* The DOM element to render to, or its id.
*
* @param {Highcharts.Options} options
* The chart options structure.
*
* @param {Highcharts.ChartCallbackFunction} [callback]
* Function to run when the chart has loaded and and all external images
* are loaded. Defining a
* [chart.events.load](https://api.highcharts.com/highcharts/chart.events.load)
* handler is equivalent.
*
* @return {Highcharts.Chart}
* Returns the Chart object.
*/
H.chart = function (a, b, c) {
return new Chart(a, b, c);
};
extend(Chart.prototype, /** @lends Highcharts.Chart.prototype */ {
// Hook for adding callbacks in modules
callbacks: [],
/**
* Handle the arguments passed to the constructor.
*
* @private
* @function Highcharts.Chart#getArgs
*
* @param {...Array<*>} arguments
* All arguments for the constructor.
*
* @return {Array<*>}
* Passed arguments without renderTo.
*
* @fires Highcharts.Chart#event:init
* @fires Highcharts.Chart#event:afterInit
*/
getArgs: function () {
var args = [].slice.call(arguments);
// Remove the optional first argument, renderTo, and
// set it on this.
if (isString(args[0]) || args[0].nodeName) {
this.renderTo = args.shift();
}
this.init(args[0], args[1]);
},
/**
* Overridable function that initializes the chart. The constructor's
* arguments are passed on directly.
*
* @function Highcharts.Chart#init
*
* @param {Highcharts.Options} userOptions
* Custom options.
*
* @param {Function} [callback]
* Function to run when the chart has loaded and and all external
* images are loaded.
*
* @return {void}
*
* @fires Highcharts.Chart#event:init
* @fires Highcharts.Chart#event:afterInit
*/
init: function (userOptions, callback) {
// Handle regular options
var options,
// skip merging data points to increase performance
seriesOptions = userOptions.series, userPlotOptions = userOptions.plotOptions || {};
// Fire the event with a default function
fireEvent(this, 'init', { args: arguments }, function () {
userOptions.series = null;
options = merge(defaultOptions, userOptions); // do the merge
// 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 optionsChart = options.chart;
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 H.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 (H.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
*
* @param {Highcharts.SeriesOptions} options
*
* @return {Highcharts.Series}
*/
initSeries: function (options) {
var chart = this, optionsChart = chart.options.chart, type = (options.type ||
optionsChart.type ||
optionsChart.defaultSeriesType), series, Constr = seriesTypes[type];
// No such series type
if (!Constr) {
H.error(17, true, chart, { missingModuleFor: type });
}
series = new Constr();
series.init(this, options);
return series;
},
/**
* Internal function to set data for all series with enabled sorting.
*
* @private
* @function Highcharts.Chart#setSeriesData
*
* @param {Highcharts.SeriesOptions} options
*
* @return {void}
*/
setSeriesData: function () {
this.getSeriesOrderByLinks().forEach(function (series) {
// We need to set data for series with sorting after series init
if (!series.points && !series.data && series.enabledDataSorting) {
series.setData(series.options.data, false);
}
});
},
/**
* Sort and return chart series in order depending on the number of linked
* series.
*
* @private
* @function Highcharts.Series#getSeriesOrderByLinks
*
* @return {Array<Highcharts.Series>}
*/
getSeriesOrderByLinks: function () {
return this.series.concat().sort(function (a, b) {
if (a.linkedSeries.length || b.linkedSeries.length) {
return b.linkedSeries.length - a.linkedSeries.length;
}
return 0;
});
},
/**
* Order all series above a given index. When series are added and ordered
* by configuration, only the last series is handled (#248, #1123, #2456,
* #6112). This function is called on series initialization and destroy.
*
* @private
* @function Highcharts.Series#orderSeries
* @param {number} [fromIndex]
* If this is given, only the series above this index are handled.
* @return {void}
*/
orderSeries: function (fromIndex) {
var series = this.series, i = fromIndex || 0;
for (; i < series.length; i++) {
if (series[i]) {
/**
* Contains the series' index in the `Chart.series` array.
*
* @name Highcharts.Series#index
* @type {number}
* @readonly
*/
series[i].index = i;
series[i].name = series[i].getName();
}
}
},
/**
* Check whether a given point is within the plot area.
*
* @function Highcharts.Chart#isInsidePlot
*
* @param {number} plotX
* Pixel x relative to the plot area.
*
* @param {number} plotY
* Pixel y relative to the plot area.
*
* @param {boolean} [inverted]
* Whether the chart is inverted.
*
* @return {boolean}
* Returns true if the given point is inside the plot area.
*/
isInsidePlot: function (plotX, plotY, inverted) {
var x = inverted ? plotY : plotX, y = inverted ? plotX : plotY;
return x >= 0 &&
x <= this.plotWidth &&
y >= 0 &&
y <= this.plotHeight;
},
/**
* Redraw the chart after changes have been done to the data, axis extremes
* chart size or chart elements. All methods for updating axes, series or
* points have a parameter for redrawing the chart. This is `true` by
* default. But in many cases you want to do more than one operation on the
* chart before redrawing, for example add a number of points. In those
* cases it is a waste of resources to redraw the chart for each new point
* added. So you add the points and call `chart.redraw()` after.
*
* @function Highcharts.Chart#redraw
*
* @param {boolean|Highcharts.AnimationOptionsObject} [animation]
* If or how to apply animation to the redraw.
*
* @return {void}
*
* @fires Highcharts.Chart#event:afterSetExtremes
* @fires Highcharts.Chart#event:beforeRedraw
* @fires Highcharts.Chart#event:predraw
* @fires Highcharts.Chart#event:redraw
* @fires Highcharts.Chart#event:render
* @fires Highcharts.Chart#event:updatedData
*/
redraw: function (animation) {
fireEvent(this, 'beforeRedraw');
var chart = this, axes = chart.axes, series = chart.series, pointer = chart.pointer, legend = chart.legend, legendUserOptions = chart.userOptions.legend, redrawLegend = chart.isDirtyLegend, hasStackedSeries, hasDirtyStacks, hasCartesianSeries = chart.hasCartesianSeries, isDirtyBox = chart.isDirtyBox, i, serie, renderer = chart.renderer, isHiddenChart = renderer.isHidden(), afterRedraw = [];
// Handle responsive rules, not only on resize (#6130)
if (chart.setResponsive) {
chart.setResponsive(false);
}
setAnimation(animation, chart);
if (isHiddenChart) {
chart.temporaryDisplay();
}
// Adjust title layout (reflow multiline text)
chart.layOutTitles();
// link stacked series
i = series.length;
while (i--) {
serie = series[i];
if (serie.options.stacking) {
hasStackedSeries = true;
if (serie.isDirty) {
hasDirtyStacks = true;
break;
}
}
}
if (hasDirtyStacks) { // mark others as dirty
i = series.length;
while (i--) {
serie = series[i];
if (serie.options.stacking) {
serie.isDirty = true;
}
}
}
// Handle updated data in the series
series.forEach(function (serie) {
if (serie.isDirty) {
if (serie.options.legendType === 'point') {
if (serie.updateTotals) {
serie.updateTotals();
}
redrawLegend = true;
}
else if (legendUserOptions &&
(legendUserOptions.labelFormatter ||
legendUserOptions.labelFormat)) {
redrawLegend = true; // #2165
}
}
if (serie.isDirtyData) {
fireEvent(serie, 'updatedData');
}
});
// handle added or removed series
if (redrawLegend && legend && legend.options.enabled) {
// draw legend graphics
legend.render();
chart.isDirtyLegend = false;
}
// reset stacks
if (hasStackedSeries) {
chart.getStacks();
}
if (hasCartesianSeries) {
// set axes scales
axes.forEach(function (axis) {
axis.updateNames();
axis.setScale();
});
}
chart.getMargins(); // #3098
if (hasCartesianSeries) {
// If one axis is dirty, all axes must be redrawn (#792, #2169)
axes.forEach(function (axis) {
if (axis.isDirty) {
isDirtyBox = true;
}
});
// redraw axes
axes.forEach(function (axis) {
// Fire 'afterSetExtremes' only if extremes are set
var key = axis.min + ',' + axis.max;
if (axis.extKey !== key) { // #821, #4452
axis.extKey = key;
// prevent a recursive call to chart.redraw() (#1119)
afterRedraw.push(function () {
fireEvent(axis, 'afterSetExtremes', extend(axis.eventArgs, axis.getExtremes())); // #747, #751
delete axis.eventArgs;
});
}
if (isDirtyBox || hasStackedSeries) {
axis.redraw();
}
});
}
// the plot areas size has changed
if (isDirtyBox) {
chart.drawChartBox();
}
// Fire an event before redrawing series, used by the boost module to
// clear previous series renderings.
fireEvent(chart, 'predraw');
// redraw affected series
series.forEach(function (serie) {
if ((isDirtyBox || serie.isDirty) && serie.visible) {
serie.redraw();
}
// Set it here, otherwise we will have unlimited 'updatedData' calls
// for a hidden series after setData(). Fixes #6012
serie.isDirtyData = false;
});
// move tooltip or reset
if (pointer) {
pointer.reset(true);
}
// redraw if canvas
renderer.draw();
// Fire the events
fireEvent(chart, 'redraw');
fireEvent(chart, 'render');
if (isHiddenChart) {
chart.temporaryDisplay(true);
}
// Fire callbacks that are put on hold until after the redraw
afterRedraw.forEach(function (callback) {
callback.call();
});
},
/**
* Get an axis, series or point object by `id` as given in the configuration
* options. Returns `undefined` if no item is found.
*
* @sample highcharts/plotoptions/series-id/
* Get series by id
*
* @function Highcharts.Chart#get
*
* @param {string} id
* The id as given in the configuration options.
*
* @return {Highcharts.Axis|Highcharts.Series|Highcharts.Point|undefined}
* The retrieved item.
*/
get: function (id) {
var ret, series = this.series, i;
/**
* @private
* @param {Highcharts.Axis|Highcharts.Series} item
* @return {boolean}
*/
function itemById(item) {
return (item.id === id ||
(item.options && item.options.id === id));
}
ret =
// Search axes
find(this.axes, itemById) ||
// Search series
find(this.series, itemById);
// Search points
for (i = 0; !ret && i < series.length; i++) {
ret = find(series[i].points || [], itemById);
}
return ret;
},
/**
* Create the Axis instances based on the config options.
*
* @private
* @function Highcharts.Chart#getAxes
*
* @return {void}
*
* @fires Highcharts.Chart#event:afterGetAxes
* @fires Highcharts.Chart#event:getAxes
*/
getAxes: function () {
var chart = this, options = this.options, xAxisOptions = options.xAxis = splat(options.xAxis || {}), yAxisOptions = options.yAxis = splat(options.yAxis || {}), optionsArray;
fireEvent(this, 'getAxes');
// make sure the options are arrays and add some members
xAxisOptions.forEach(function (axis, i) {
axis.index = i;
axis.isX = true;
});
yAxisOptions.forEach(function (axis, i) {
axis.index = i;
});
// concatenate all axis options into one array
optionsArray = xAxisOptions.concat(yAxisOptions);
optionsArray.forEach(function (axisOptions) {
new Axis(chart, axisOptions); // eslint-disable-line no-new
});
fireEvent(this, 'afterGetAxes');
},
/**
* Returns an array of all currently selected points in the chart. Points
* can be selected by clicking or programmatically by the
* {@link Highcharts.Point#select}
* function.
*
* @sample highcharts/plotoptions/series-allowpointselect-line/
* Get selected points
*
* @function Highcharts.Chart#getSelectedPoints
*
* @return {Array<Highcharts.Point>}
* The currently selected points.
*/
getSelectedPoints: function () {
var points = [];
this.series.forEach(function (serie) {
// For one-to-one points inspect series.data in order to retrieve
// points outside the visible range (#6445). For grouped data,
// inspect the generated series.points.
points = points.concat((serie[serie.hasGroupedData ? 'points' : 'data'] || []).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.
*/
getSelectedSeries: function () {
return this.series.filter(function (serie) {
return serie.selected;
});
},
/**
* Set a new title or subtitle for the chart.
*
* @sample highcharts/members/chart-settitle/
* Set title text and styles
*
* @function Highcharts.Chart#setTitle
*
* @param {Highcharts.TitleOptions} [titleOptions]
* New title options. The title text itself is set by the
* `titleOptions.text` property.
*
* @param {Highcharts.SubtitleOptions} [subtitleOptions]
* New subtitle options. The subtitle text itself is set by the
* `subtitleOptions.text` property.
*
* @param {boolean} [redraw]
* Whether to redraw the chart or wait for a later call to
* `chart.redraw()`.
*
* @return {void}
*/
setTitle: function (titleOptions, subtitleOptions, redraw) {
this.applyDescription('title', titleOptions);
this.applyDescription('subtitle', subtitleOptions);
// The initial call also adds the caption. On update, chart.update will
// relay to Chart.setCaption.
this.applyDescription('caption', void 0);
this.layOutTitles(redraw);
},
/**
* Apply a title, subtitle or caption for the chart
*
* @private
* @function Highcharts.Chart#applyDescription
*
* @param name {string}
* Either title, subtitle or caption
* @param {Highcharts.TitleOptions|Highcharts.SubtitleOptions|Highcharts.CaptionOptions|undefined} explicitOptions
* The options to set, will be merged with default options.
*
* @return {void}
*/
applyDescription: function (name, explicitOptions) {
var chart = this;
// Default style
var style = name === 'title' ? {
color: '#333333',
fontSize: this.options.isStock ? '16px' : '18px' // #2944
} : {
color: '#666666'
};
// Merge default options with explicit options
var options = this.options[name] = merge(
// Default styles
(!this.styledMode && { style: style }), this.options[name], explicitOptions);
var elem = this[name];
if (elem && explicitOptions) {
this[name] = elem = elem.destroy(); // remove old
}
if (options && !elem) {
elem = this.renderer.text(options.text, 0, 0, options.useHTML)
.attr({
align: options.align,
'class': 'highcharts-' + name,
zIndex: options.zIndex || 4
})
.add();
// Update methods, shortcut to Chart.setTitle, Chart.setSubtitle and
// Chart.setCaption
elem.update = function (updateOptions) {
var fn = {
title: 'setTitle',
subtitle: 'setSubtitle',
caption: 'setCaption'
}[name];
chart[fn](updateOptions);
};
// Presentational
if (!this.styledMode) {
elem.css(options.style);
}
/**
* The chart title. The title has an `update` method that allows
* modifying the options directly or indirectly via
* `chart.update`.
*
* @sample highcharts/members/title-update/
* Updating titles
*
* @name Highcharts.Chart#title
* @type {Highcharts.TitleObject}
*/
/**
* The chart subtitle. The subtitle has an `update` method that
* allows modifying the options directly or indirectly via
* `chart.update`.
*
* @name Highcharts.Chart#subtitle
* @type {Highcharts.SubtitleObject}
*/
this[name] = elem;
}
},
/**
* Internal function to lay out the chart title, subtitle and caption, and
* cache the full offset height for use in `getMargins`. The result is
* stored in `this.titleOffset`.
*
* @private
* @function Highcharts.Chart#layOutTitles
*
* @param {boolean} [redraw=true]
*
* @return {void}
*
* @fires Highcharts.Chart#event:afterLayOutTitles
*/
layOutTitles: function (redraw) {
var titleOffset = [0, 0, 0], requiresDirtyBox, renderer = this.renderer, spacingBox = this.spacingBox;
// Lay out the title and the subtitle respectively
['title', 'subtitle', 'caption'].forEach(function (key) {
var title = this[key], titleOptions = this.options[key], verticalAlign = titleOptions.verticalAlign || 'top', offset = key === 'title' ? -3 :
// Floating subtitle (#6574)
verticalAlign === 'top' ? titleOffset[0] + 2 : 0, titleSize, height;
if (title) {
if (!this.styledMode) {
titleSize = titleOptions.style.fontSize;
}
titleSize = renderer.fontMetrics(titleSize, title).b;
title
.css({
width: (titleOptions.width ||
spacingBox.width + (titleOptions.widthAdjust || 0)) + 'px'
});
// Skip the cache for HTML (#3481, #11666)
height = Math.round(title.getBBox(titleOptions.useHTML).height);
title.align(extend({
y: verticalAlign === 'bottom' ?
titleSize :
offset + titleSize,
height: height
}, titleOptions), false, 'spacingBox');
if (!titleOptions.floating) {
if (verticalAlign === 'top') {
titleOffset[0] = Math.ceil(titleOffset[0] +
height);
}
else if (verticalAlign === 'bottom') {
titleOffset[2] = Math.ceil(titleOffset[2] +
height);
}
}
}
}, this);
// Handle title.margin and caption.margin
if (titleOffset[0] &&
(this.options.title.verticalAlign || 'top') === 'top') {
titleOffset[0] += this.options.title.margin;
}
if (titleOffset[2] &&
this.options.caption.verticalAlign === 'bottom') {
titleOffset[2] += this.options.caption.margin;
}
requiresDirtyBox = (!this.titleOffset ||
this.titleOffset.join(',') !== titleOffset.join(','));
// Used in getMargins
this.titleOffset = titleOffset;
fireEvent(this, 'afterLayOutTitles');
if (!this.isDirtyBox && requiresDirtyBox) {
this.isDirtyBox = this.isDirtyLegend = requiresDirtyBox;
// Redraw if necessary (#2719, #2744)
if (this.hasRendered && pick(redraw, true) && this.isDirtyBox) {
this.redraw();
}
}
},
/**
* Internal function to get the chart width and height according to options
* and container size. Sets {@link Chart.chartWidth} and
* {@link Chart.chartHeight}.
*
* @private
* @function Highcharts.Chart#getChartSize
*
* @return {void}
*/
getChartSize: function () {
var chart = this, optionsChart = chart.options.chart, widthOption = optionsChart.width, heightOption = optionsChart.height, renderTo = chart.renderTo;
// Get inner width and height
if (!defined(widthOption)) {
chart.containerWidth = H.getStyle(renderTo, 'width');
}
if (!defined(heightOption)) {
chart.containerHeight = H.getStyle(renderTo, 'height');
}
/**
* The current pixel width of the chart.
*
* @name Highcharts.Chart#chartWidth
* @type {number}
*/
chart.chartWidth = Math.max(// #1393
0, widthOption || chart.containerWidth || 600 // #1460
);
/**
* The current pixel height of the chart.
*
* @name Highcharts.Chart#chartHeight
* @type {number}
*/
chart.chartHeight = Math.max(0, relativeLength(heightOption, chart.chartWidth) ||
(chart.containerHeight > 1 ?
chart.containerHeight :
400));
},
/**
* If the renderTo element has no offsetWidth, most likely one or more of
* its parents are hidden. Loop up the DOM tree to temporarily display the
* parents, then save the original display properties, and when the true
* size is retrieved, reset them. Used on first render and on redraws.
*
* @private
* @function Highcharts.Chart#temporaryDisplay
*
* @param {boolean} [revert]
* Revert to the saved original styles.
*
* @return {void}
*/
temporaryDisplay: function (revert) {
var node = this.renderTo, tempStyle;
if (!revert) {
while (node && node.style) {
// When rendering to a detached node, it needs to be temporarily
// attached in order to read styling and bounding boxes (#5783,
// #7024).
if (!doc.body.contains(node) && !node.parentNode) {
node.hcOrigDetached = true;
doc.body.appendChild(node);
}
if (H.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;
}
H.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) {
H.css(node, node.hcOrigStyle);
delete node.hcOrigStyle;
}
if (node.hcOrigDetached) {
doc.body.removeChild(node);
node.hcOrigDetached = false;
}
node = node.parentNode;
}
}
},
/**
* Set the {@link Chart.container|chart container's} class name, in
* addition to `highcharts-container`.
*
* @function Highcharts.Chart#setClassName
*
* @param {string} [className]
*
* @return {void}
*/
setClassName: function (className) {
this.container.className = 'highcharts-container ' + (className || '');
},
/**
* Get the containing element, determine the size and create the inner
* container div to hold the chart.
*
* @private
* @function Highcharts.Chart#afterGetContainer
*
* @return {void}
*
* @fires Highcharts.Chart#event:afterGetContainer
*/
getContainer: function () {
var chart = this, container, options = chart.options, optionsChart = options.chart, chartWidth, chartHeight, renderTo = chart.renderTo, indexAttrName = 'data-highcharts-chart', oldChartIndex, Ren, containerId = H.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) {
H.error(13, true, chart);
}
// If the container already holds a chart, destroy it. The check for
// hasRendered is there because web pages that are saved to disk from
// the browser, will preserve the data-highcharts-chart attribute and
// the SVG contents, but not an interactive chart. So in this case,
// charts[oldChartIndex] will point to the wrong chart if any (#2609).
oldChartIndex = pInt(attr(renderTo, indexAttrName));
if (isNumber(oldChartIndex) &&
charts[oldChartIndex] &&
charts[oldChartIndex].hasRendered) {
charts[oldChartIndex].destroy();
}
// Make a reference to the chart from the div
attr(renderTo, indexAttrName, chart.index);
// remove previous chart
renderTo.innerHTML = '';
// If the container doesn't have an offsetWidth, it has or is a child of
// a node that has display:none. We need to temporarily move it out to a
// visible state to determine the size, else the legend and tooltips
// won't render properly. The skipClone option is used in sparklines as
// a micro optimization, saving about 1-2 ms each chart.
if (!optionsChart.skipClone && !renderTo.offsetWidth) {
chart.temporaryDisplay();
}
// get the width and height
chart.getChartSize();
chartWidth = chart.chartWidth;
chartHeight = chart.chartHeight;
// Allow table cells and flex-boxes to shrink without the chart blocking
// them out (#6427)
css(renderTo, { overflow: 'hidden' });
// Create the inner container
if (!chart.styledMode) {
containerStyle = extend({
position: 'relative',
// needed for context menu (avoidscrollbars) and content
// overflow in IE
overflow: 'hidden',
width: chartWidth + 'px',
height: chartHeight + 'px',
textAlign: 'left',
lineHeight: 'normal',
zIndex: 0,
'-webkit-tap-highlight-color': 'rgba(0,0,0,0)'
}, optionsChart.style);
}
/**
* The containing HTML element of the chart. The container is
* dynamically inserted into the element given as the `renderTo`
* parameter in the {@link Highcharts#chart} constructor.
*
* @name Highcharts.Chart#container
* @type {Highcharts.HTMLDOMElement}
*/
container = createElement('div', {
id: containerId
}, containerStyle, renderTo);
chart.container = container;
// cache the cursor (#1650)
chart._cursor = container.style.cursor;
// Initialize the renderer
Ren = H[optionsChart.renderer] || H.Renderer;
/**
* The renderer instance of the chart. Each chart instance has only one
* associated renderer.
*
* @name Highcharts.Chart#renderer
* @type {Highcharts.SVGRenderer}
*/
chart.renderer = new Ren(container, chartWidth, chartHeight, null, optionsChart.forExport, options.exporting && options.exporting.allowHTML, chart.styledMode);
chart.setClassName(optionsChart.className);
if (!chart.styledMode) {
chart.renderer.setStyle(optionsChart.style);
}
else {
// Initialize definitions
for (key in options.defs) { // eslint-disable-line guard-for-in
this.renderer.definition(options.defs[key]);
}
}
// Add a reference to the charts index
chart.renderer.chartIndex = chart.index;
fireEvent(this, 'afterGetContainer');
},
/**
* Calculate margins by rendering axis labels in a preliminary position.
* Title, subtitle and legend have already been rendered at this stage, but
* will be moved into their final positions.
*
* @private
* @function Highcharts.Chart#getMargins
* @param {boolean} skipAxes
* @return {void}
* @fires Highcharts.Chart#event:getMargins
*/
getMargins: function (skipAxes) {
var _a = this, spacing = _a.spacing, margin = _a.margin, titleOffset = _a.titleOffset;
this.resetMargins();
// Adjust for title and subtitle
if (titleOffset[0] && !defined(margin[0])) {
this.plotTop = Math.max(this.plotTop, titleOffset[0] + spacing[0]);
}
if (titleOffset[2] && !defined(margin[2])) {
this.marginBottom = Math.max(this.marginBottom, titleOffset[2] + spacing[2]);
}
// Adjust for legend
if (this.legend && this.legend.display) {
this.legend.adjustMargins(margin, spacing);
}
fireEvent(this, 'getMargins');
if (!skipAxes) {
this.getAxisMargins();
}
},
/**
* @private
* @function Highcharts.Chart#getAxisMargins
* @return {void}
*/
getAxisMargins: function () {
var chart = this,
// [top, right, bottom, left]
axisOffset = chart.axisOffset = [0, 0, 0, 0], colorAxis = chart.colorAxis, margin = chart.margin, getOffset = function (axes) {
axes.forEach(function (axis) {
if (axis.visible) {
axis.getOffset();
}
});
};
// pre-render axes to get labels offset width
if (chart.hasCartesianSeries) {
getOffset(chart.axes);
}
else if (colorAxis && colorAxis.length) {
getOffset(colorAxis);
}
// Add the axis offsets
marginNames.forEach(function (m, side) {
if (!defined(margin[side])) {
chart[m] += axisOffset[side];
}
});
chart.setChartSize();
},
/**
* Reflows the chart to its container. By default, the chart reflows
* automatically to its container following a `window.resize` event, as per
* the [chart.reflow](https://api.highcharts.com/highcharts/chart.reflow)
* option. However, there are no reliable events for div resize, so if the
* container is resized without a window resize event, this must be called
* explicitly.
*
* @sample highcharts/members/chart-reflow/
* Resize div and reflow
* @sample highcharts/chart/events-container/
* Pop up and reflow
*
* @function Highcharts.Chart#reflow
*
* @param {global.Event} [e]
* Event arguments. Used primarily when the function is called
* internally as a response to window resize.
*/
reflow: function (e) {
var chart = this, optionsChart = chart.options.chart, renderTo = chart.renderTo, hasUserSize = (defined(optionsChart.width) &&
defined(optionsChart.height)), width = optionsChart.width || H.getStyle(renderTo, 'width'), height = optionsChart.height || H.getStyle(renderTo, 'height'), target = e ? e.target : win;
// Width and height checks for display:none. Target is doc in IE8 and
// Opera, win in Firefox, Chrome and IE9.
if (!hasUserSize &&
!chart.isPrinting &&
width &&
height &&
(target === win || target === doc)) {
if (width !== chart.containerWidth ||
height !== chart.containerHeight) {
H.clearTimeout(chart.reflowTimeout);
// When called from window.resize, e is set, else it's called
// directly (#2224)
chart.reflowTimeout = syncTimeout(function () {
// Set size, it may have been destroyed in the meantime
// (#1257)
if (chart.container) {
chart.setSize(void 0, void 0, false);
}
}, e ? 100 : 0);
}
chart.containerWidth = width;
chart.containerHeight = height;
}
},
/**
* Toggle the event handlers necessary for auto resizing, depending on the
* `chart.reflow` option.
*
* @private
* @function Highcharts.Chart#setReflow
* @param {boolean} [reflow]
* @return {void}
*/
setReflow: function (reflow) {
var chart = this;
if (reflow !== false && !this.unbindReflow) {
this.unbindReflow = addEvent(win, 'resize', function (e) {
// a removed event listener still runs in Edge and IE if the
// listener was removed while the event runs, so check if the
// chart is not destroyed (#11609)
if (chart.options) {
chart.reflow(e);
}
});
addEvent(this, 'destroy', this.unbindReflow);
}
else if (reflow === false && this.unbindReflow) {
// Unbind and unset
this.unbindReflow = this.unbindReflow();
}
// The following will add listeners to re-fit the chart before and after
// printing (#2284). However it only works in WebKit. Should have worked
// in Firefox, but not supported in IE.
/*
if (win.matchMedia) {
win.matchMedia('print').addListener(function reflow() {
chart.reflow();
});
}
//*/
},
/**
* Resize the chart to a given width and height. In order to set the width
* only, the height argument may be skipped. To set the height only, pass
* `undefined` for the width.
*
* @sample highcharts/members/chart-setsize-button/
* Test resizing from buttons
* @sample highcharts/members/chart-setsize-jquery-resizable/
* Add a jQuery UI resizable
* @sample stock/members/chart-setsize/
* Highstock with UI resizable
*
* @function Highcharts.Chart#setSize
*
* @param {number|null} [width]
* The new pixel width of the chart. Since v4.2.6, the argument can
* be `undefined` in order to preserve the current value (when
* setting height only), or `null` to adapt to the width of the
* containing element.
*
* @param {number|null} [height]
* The new pixel height of the chart. Since v4.2.6, the argument can
* be `undefined` in order to preserve the current value, or `null`
* in order to adapt to the height of the containing element.
*
* @param {boolean|Highcharts.AnimationOptionsObject} [animation=true]
* Whether and how to apply animation.
*
* @return {void}
*
* @fires Highcharts.Chart#event:endResize
* @fires Highcharts.Chart#event:resize
*/
setSize: function (width, height, animation) {
var chart = this,