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