highcharts
Version:
JavaScript charting framework
1,199 lines • 132 kB
JavaScript
/* *
*
* (c) 2010-2025 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import A from '../Animation/AnimationUtilities.js';
const { animObject } = A;
import AxisDefaults from './AxisDefaults.js';
const { xAxis, yAxis } = AxisDefaults;
import Color from '../Color/Color.js';
import D from '../Defaults.js';
const { defaultOptions } = D;
import F from '../Foundation.js';
const { registerEventOptions } = F;
import H from '../Globals.js';
const { deg2rad } = H;
import Tick from './Tick.js';
import U from '../Utilities.js';
const { arrayMax, arrayMin, clamp, correctFloat, defined, destroyObjectProperties, erase, error, extend, fireEvent, getClosestDistance, insertItem, isArray, isNumber, isString, merge, normalizeTickInterval, objectEach, pick, relativeLength, removeEvent, splat, syncTimeout } = U;
const getNormalizedTickInterval = (axis, tickInterval) => normalizeTickInterval(tickInterval, void 0, void 0, pick(axis.options.allowDecimals,
// If the tick interval is greater than 0.5, avoid decimals, as
// linear axes are often used to render discrete values (#3363). If
// a tick amount is set, allow decimals by default, as it increases
// the chances for a good fit.
tickInterval < 0.5 || axis.tickAmount !== void 0), !!axis.tickAmount);
extend(defaultOptions, { xAxis, yAxis: merge(xAxis, yAxis) });
/* *
*
* Class
*
* */
/**
* Create a new axis object. Called internally when instantiating a new chart or
* adding axes by {@link Highcharts.Chart#addAxis}.
*
* A chart can have from 0 axes (pie chart) to multiples. In a normal, single
* series cartesian chart, there is one X axis and one Y axis.
*
* The X axis or axes are referenced by {@link Highcharts.Chart.xAxis}, which is
* an array of Axis objects. If there is only one axis, it can be referenced
* through `chart.xAxis[0]`, and multiple axes have increasing indices. The same
* pattern goes for Y axes.
*
* If you need to get the axes from a series object, use the `series.xAxis` and
* `series.yAxis` properties. These are not arrays, as one series can only be
* associated to one X and one Y axis.
*
* A third way to reference the axis programmatically is by `id`. Add an `id` in
* the axis configuration options, and get the axis by
* {@link Highcharts.Chart#get}.
*
* Configuration options for the axes are given in options.xAxis and
* options.yAxis.
*
* @class
* @name Highcharts.Axis
*
* @param {Highcharts.Chart} chart
* The Chart instance to apply the axis on.
*
* @param {Highcharts.AxisOptions} userOptions
* Axis options
*/
class Axis {
/* *
*
* Constructors
*
* */
constructor(chart, userOptions, coll) {
this.init(chart, userOptions, coll);
}
/* *
*
* Functions
*
* */
/**
* Overrideable function to initialize the axis.
*
* @see {@link Axis}
*
* @function Highcharts.Axis#init
*
* @param {Highcharts.Chart} chart
* The Chart instance to apply the axis on.
*
* @param {AxisOptions} userOptions
* Axis options.
*
* @emits Highcharts.Axis#event:afterInit
* @emits Highcharts.Axis#event:init
*/
init(chart, userOptions, coll = this.coll) {
const isXAxis = coll === 'xAxis', axis = this, horiz = axis.isZAxis || (chart.inverted ? !isXAxis : isXAxis);
/**
* The Chart that the axis belongs to.
*
* @name Highcharts.Axis#chart
* @type {Highcharts.Chart}
*/
axis.chart = chart;
/**
* Whether the axis is horizontal.
*
* @name Highcharts.Axis#horiz
* @type {boolean|undefined}
*/
axis.horiz = horiz;
/**
* Whether the axis is the x-axis.
*
* @name Highcharts.Axis#isXAxis
* @type {boolean|undefined}
*/
axis.isXAxis = isXAxis;
/**
* The collection where the axis belongs, for example `xAxis`, `yAxis`
* or `colorAxis`. Corresponds to properties on Chart, for example
* {@link Chart.xAxis}.
*
* @name Highcharts.Axis#coll
* @type {string}
*/
axis.coll = coll;
fireEvent(this, 'init', { userOptions: userOptions });
// Needed in setOptions
axis.opposite = pick(userOptions.opposite, axis.opposite);
/**
* The side on which the axis is rendered. 0 is top, 1 is right, 2
* is bottom and 3 is left.
*
* @name Highcharts.Axis#side
* @type {number}
*/
axis.side = pick(userOptions.side, axis.side, (horiz ?
(axis.opposite ? 0 : 2) : // Top : bottom
(axis.opposite ? 1 : 3)) // Right : left
);
/**
* Current options for the axis after merge of defaults and user's
* options.
*
* @name Highcharts.Axis#options
* @type {Highcharts.AxisOptions}
*/
axis.setOptions(userOptions);
const options = axis.options, labelsOptions = options.labels;
// Set the type and fire an event
axis.type ?? (axis.type = options.type || 'linear');
axis.uniqueNames ?? (axis.uniqueNames = options.uniqueNames ?? true);
fireEvent(axis, 'afterSetType');
/**
* User's options for this axis without defaults.
*
* @name Highcharts.Axis#userOptions
* @type {Highcharts.AxisOptions}
*/
axis.userOptions = userOptions;
axis.minPixelPadding = 0;
/**
* Whether the axis is reversed. Based on the `axis.reversed`,
* option, but inverted charts have reversed xAxis by default.
*
* @name Highcharts.Axis#reversed
* @type {boolean}
*/
axis.reversed = pick(options.reversed, axis.reversed);
axis.visible = options.visible;
axis.zoomEnabled = options.zoomEnabled;
// Initial categories
axis.hasNames = this.type === 'category' || options.categories === true;
/**
* If categories are present for the axis, names are used instead of
* numbers for that axis.
*
* Since Highcharts 3.0, categories can also be extracted by giving each
* point a name and setting axis type to `category`. However, if you
* have multiple series, best practice remains defining the `categories`
* array.
*
* @see [xAxis.categories](/highcharts/xAxis.categories)
*
* @name Highcharts.Axis#categories
* @type {Array<string>}
* @readonly
*/
axis.categories = (isArray(options.categories) && options.categories) ||
(axis.hasNames ? [] : void 0);
if (!axis.names) { // Preserve on update (#3830)
axis.names = [];
axis.names.keys = {};
}
// Placeholder for plotlines and plotbands groups
axis.plotLinesAndBandsGroups = {};
// Shorthand types
axis.positiveValuesOnly = !!axis.logarithmic;
// Flag, if axis is linked to another axis
axis.isLinked = defined(options.linkedTo);
/**
* List of major ticks mapped by position on axis.
*
* @see {@link Highcharts.Tick}
*
* @name Highcharts.Axis#ticks
* @type {Highcharts.Dictionary<Highcharts.Tick>}
*/
axis.ticks = {};
axis.labelEdge = [];
/**
* List of minor ticks mapped by position on the axis.
*
* @see {@link Highcharts.Tick}
*
* @name Highcharts.Axis#minorTicks
* @type {Highcharts.Dictionary<Highcharts.Tick>}
*/
axis.minorTicks = {};
// List of plotLines/Bands
axis.plotLinesAndBands = [];
// Alternate bands
axis.alternateBands = {};
/**
* The length of the axis in terms of pixels.
*
* @name Highcharts.Axis#len
* @type {number}
*/
axis.len ?? (axis.len = 0);
axis.minRange = axis.userMinRange = options.minRange || options.maxZoom;
axis.range = options.range;
axis.offset = options.offset || 0;
/**
* The maximum value of the axis. In a logarithmic axis, this is the
* logarithm of the real value, and the real value can be obtained from
* {@link Axis#getExtremes}.
*
* @name Highcharts.Axis#max
* @type {number|undefined}
*/
axis.max = void 0;
/**
* The minimum value of the axis. In a logarithmic axis, this is the
* logarithm of the real value, and the real value can be obtained from
* {@link Axis#getExtremes}.
*
* @name Highcharts.Axis#min
* @type {number|undefined}
*/
axis.min = void 0;
/**
* The processed crosshair options.
*
* @name Highcharts.Axis#crosshair
* @type {boolean|Highcharts.AxisCrosshairOptions}
*/
const crosshair = pick(options.crosshair, splat(chart.options.tooltip.crosshairs)[isXAxis ? 0 : 1]);
axis.crosshair = crosshair === true ? {} : crosshair;
// Register. Don't add it again on Axis.update().
if (chart.axes.indexOf(axis) === -1) { //
if (isXAxis) { // #2713
chart.axes.splice(chart.xAxis.length, 0, axis);
}
else {
chart.axes.push(axis);
}
insertItem(this, chart[this.coll]);
}
chart.orderItems(axis.coll);
/**
* All series associated to the axis.
*
* @name Highcharts.Axis#series
* @type {Array<Highcharts.Series>}
*/
axis.series = axis.series || []; // Populated by Series
// Reversed axis
if (chart.inverted &&
!axis.isZAxis &&
isXAxis &&
!defined(axis.reversed)) {
axis.reversed = true;
}
axis.labelRotation = isNumber(labelsOptions.rotation) ?
labelsOptions.rotation :
void 0;
// Register event listeners
registerEventOptions(axis, options);
fireEvent(this, 'afterInit');
}
/**
* Merge and set options.
*
* @private
* @function Highcharts.Axis#setOptions
*
* @param {Highcharts.AxisOptions} userOptions
* Axis options.
*
* @emits Highcharts.Axis#event:afterSetOptions
*/
setOptions(userOptions) {
const sideSpecific = this.horiz ?
// Top and bottom axis defaults
{
labels: {
autoRotation: [-45],
padding: 3
},
margin: 15
} :
// Left and right axis, title rotated 90 or 270 degrees
// respectively
{
labels: {
padding: 1
},
title: {
rotation: 90 * this.side
}
};
this.options = merge(sideSpecific,
// Merge in the default title for y-axis, which changes with
// language settings
this.coll === 'yAxis' ? {
title: {
text: this.chart.options.lang.yAxisTitle
}
} : {}, defaultOptions[this.coll], userOptions);
fireEvent(this, 'afterSetOptions', { userOptions });
}
/**
* The default label formatter. The context is a special config object for
* the label. In apps, use the
* [labels.formatter](https://api.highcharts.com/highcharts/xAxis.labels.formatter)
* instead, except when a modification is needed.
*
* @function Highcharts.Axis#defaultLabelFormatter
*
* @param {Highcharts.AxisLabelsFormatterContextObject} this
* Formatter context of axis label.
*
* @param {Highcharts.AxisLabelsFormatterContextObject} [ctx]
* Formatter context of axis label.
*
* @return {string}
* The formatted label content.
*/
defaultLabelFormatter() {
const axis = this.axis, chart = this.chart, { numberFormatter } = chart, value = isNumber(this.value) ? this.value : NaN, time = axis.chart.time, categories = axis.categories, dateTimeLabelFormat = this.dateTimeLabelFormat, lang = defaultOptions.lang, numericSymbols = lang.numericSymbols, numSymMagnitude = lang.numericSymbolMagnitude || 1000,
// Make sure the same symbol is added for all labels on a linear
// axis
numericSymbolDetector = axis.logarithmic ?
Math.abs(value) :
axis.tickInterval;
let i = numericSymbols?.length, multi, ret;
if (categories) {
ret = `${this.value}`;
}
else if (dateTimeLabelFormat) { // Datetime axis
ret = time.dateFormat(dateTimeLabelFormat, value, true);
}
else if (i &&
numericSymbols &&
numericSymbolDetector >= 1000) {
// Decide whether we should add a numeric symbol like k (thousands)
// or M (millions). If we are to enable this in tooltip or other
// places as well, we can move this logic to the numberFormatter and
// enable it by a parameter.
while (i-- && typeof ret === 'undefined') {
multi = Math.pow(numSymMagnitude, i + 1);
if (
// Only accept a numeric symbol when the distance is more
// than a full unit. So for example if the symbol is k, we
// don't accept numbers like 0.5k.
numericSymbolDetector >= multi &&
// Accept one decimal before the symbol. Accepts 0.5k but
// not 0.25k. How does this work with the previous?
(value * 10) % multi === 0 &&
numericSymbols[i] !== null &&
value !== 0) { // #5480
ret = numberFormatter(value / multi, -1) + numericSymbols[i];
}
}
}
if (typeof ret === 'undefined') {
if (Math.abs(value) >= 10000) { // Add thousands separators
ret = numberFormatter(value, -1);
}
else { // Small numbers
ret = numberFormatter(value, -1, void 0, ''); // #2466
}
}
return ret;
}
/**
* Get the minimum and maximum for the series of each axis. The function
* analyzes the axis series and updates `this.dataMin` and `this.dataMax`.
*
* @private
* @function Highcharts.Axis#getSeriesExtremes
*
* @emits Highcharts.Axis#event:afterGetSeriesExtremes
* @emits Highcharts.Axis#event:getSeriesExtremes
*/
getSeriesExtremes() {
const axis = this;
let xExtremes;
fireEvent(this, 'getSeriesExtremes', null, function () {
axis.hasVisibleSeries = false;
// Reset properties in case we're redrawing (#3353)
axis.dataMin = axis.dataMax = axis.threshold = void 0;
axis.softThreshold = !axis.isXAxis;
// Loop through this axis' series
axis.series.forEach((series) => {
if (series.reserveSpace()) {
const seriesOptions = series.options;
let xData, threshold = seriesOptions.threshold, seriesDataMin, seriesDataMax;
axis.hasVisibleSeries = true;
// Validate threshold in logarithmic axes
if (axis.positiveValuesOnly && (threshold || 0) <= 0) {
threshold = void 0;
}
// Get dataMin and dataMax for X axes
if (axis.isXAxis) {
xData = series.getColumn('x');
if (xData.length) {
xData = axis.logarithmic ?
xData.filter((x) => x > 0) :
xData;
xExtremes = series.getXExtremes(xData);
// If xData contains values which is not numbers,
// then filter them out. To prevent performance hit,
// we only do this after we have already found
// seriesDataMin because in most cases all data is
// valid. #5234.
seriesDataMin = xExtremes.min;
seriesDataMax = xExtremes.max;
if (!isNumber(seriesDataMin) &&
// #5010:
!(seriesDataMin instanceof Date)) {
xData = xData.filter(isNumber);
xExtremes = series.getXExtremes(xData);
// Do it again with valid data
seriesDataMin = xExtremes.min;
seriesDataMax = xExtremes.max;
}
if (xData.length) {
axis.dataMin = Math.min(pick(axis.dataMin, seriesDataMin), seriesDataMin);
axis.dataMax = Math.max(pick(axis.dataMax, seriesDataMax), seriesDataMax);
}
}
// Get dataMin and dataMax for Y axes, as well as handle
// stacking and processed data
}
else {
// Get this particular series extremes
const dataExtremes = series.applyExtremes();
// Get the dataMin and dataMax so far. If percentage is
// used, the min and max are always 0 and 100. If
// seriesDataMin and seriesDataMax is null, then series
// doesn't have active y data, we continue with nulls
if (isNumber(dataExtremes.dataMin)) {
seriesDataMin = dataExtremes.dataMin;
axis.dataMin = Math.min(pick(axis.dataMin, seriesDataMin), seriesDataMin);
}
if (isNumber(dataExtremes.dataMax)) {
seriesDataMax = dataExtremes.dataMax;
axis.dataMax = Math.max(pick(axis.dataMax, seriesDataMax), seriesDataMax);
}
// Adjust to threshold
if (defined(threshold)) {
axis.threshold = threshold;
}
// If any series has a hard threshold, it takes
// precedence
if (!seriesOptions.softThreshold ||
axis.positiveValuesOnly) {
axis.softThreshold = false;
}
}
}
});
});
fireEvent(this, 'afterGetSeriesExtremes');
}
/**
* Translate from axis value to pixel position on the chart, or back. Use
* the `toPixels` and `toValue` functions in applications.
*
* @private
* @function Highcharts.Axis#translate
*/
translate(val, backwards, cvsCoord, old, handleLog, pointPlacement) {
const axis = (this.linkedParent || this), // #1417
localMin = (old && axis.old ? axis.old.min : axis.min);
if (!isNumber(localMin)) {
return NaN;
}
const minPixelPadding = axis.minPixelPadding, doPostTranslate = (axis.isOrdinal ||
axis.brokenAxis?.hasBreaks ||
(axis.logarithmic && handleLog)) && axis.lin2val;
let sign = 1, cvsOffset = 0, localA = old && axis.old ? axis.old.transA : axis.transA, returnValue = 0;
if (!localA) {
localA = axis.transA;
}
// In vertical axes, the canvas coordinates start from 0 at the top like
// in SVG.
if (cvsCoord) {
sign *= -1; // Canvas coordinates inverts the value
cvsOffset = axis.len;
}
// Handle reversed axis
if (axis.reversed) {
sign *= -1;
cvsOffset -= sign * (axis.sector || axis.len);
}
// From pixels to value
if (backwards) { // Reverse translation
val = val * sign + cvsOffset;
val -= minPixelPadding;
// From chart pixel to value:
returnValue = val / localA + localMin;
if (doPostTranslate) { // Log, ordinal and broken axis
returnValue = axis.lin2val(returnValue);
}
// From value to pixels
}
else {
if (doPostTranslate) { // Log, ordinal and broken axis
val = axis.val2lin(val);
}
const value = sign * (val - localMin) * localA;
returnValue = value +
cvsOffset +
(sign * minPixelPadding) +
(isNumber(pointPlacement) ? localA * pointPlacement : 0);
if (!axis.isRadial) {
returnValue = correctFloat(returnValue);
}
}
return returnValue;
}
/**
* Translate a value in terms of axis units into pixels within the chart.
*
* @function Highcharts.Axis#toPixels
*
* @param {number|string} value
* A value in terms of axis units. For datetime axes, a timestamp or
* date/time string is accepted.
*
* @param {boolean} [paneCoordinates=false]
* Whether to return the pixel coordinate relative to the chart or just the
* axis/pane itself.
*
* @return {number}
* Pixel position of the value on the chart or axis.
*/
toPixels(value, paneCoordinates) {
return this.translate(this.chart?.time.parse(value) ?? NaN, false, !this.horiz, void 0, true) + (paneCoordinates ? 0 : this.pos);
}
/**
* Translate a pixel position along the axis to a value in terms of axis
* units.
*
* @function Highcharts.Axis#toValue
*
* @param {number} pixel
* The pixel value coordinate.
*
* @param {boolean} [paneCoordinates=false]
* Whether the input pixel is relative to the chart or just the axis/pane
* itself.
*
* @return {number}
* The axis value.
*/
toValue(pixel, paneCoordinates) {
return this.translate(pixel - (paneCoordinates ? 0 : this.pos), true, !this.horiz, void 0, true);
}
/**
* Create the path for a plot line that goes from the given value on
* this axis, across the plot to the opposite side. Also used internally for
* grid lines and crosshairs.
*
* @function Highcharts.Axis#getPlotLinePath
*
* @param {Highcharts.AxisPlotLinePathOptionsObject} options
* Options for the path.
*
* @return {Highcharts.SVGPathArray|null}
* The SVG path definition for the plot line.
*/
getPlotLinePath(options) {
const axis = this, chart = axis.chart, axisLeft = axis.left, axisTop = axis.top, old = options.old, value = options.value, lineWidth = options.lineWidth, cHeight = (old && chart.oldChartHeight) || chart.chartHeight, cWidth = (old && chart.oldChartWidth) || chart.chartWidth, transB = axis.transB;
let translatedValue = options.translatedValue, force = options.force, x1, y1, x2, y2, skip;
// eslint-disable-next-line valid-jsdoc
/**
* Check if x is between a and b. If not, either move to a/b
* or skip, depending on the force parameter.
* @private
*/
function between(x, a, b) {
if (force !== 'pass' && (x < a || x > b)) {
if (force) {
x = clamp(x, a, b);
}
else {
skip = true;
}
}
return x;
}
const evt = {
value,
lineWidth,
old,
force,
acrossPanes: options.acrossPanes,
translatedValue
};
fireEvent(this, 'getPlotLinePath', evt, function (e) {
translatedValue = pick(translatedValue, axis.translate(value, void 0, void 0, old));
// Keep the translated value within sane bounds, and avoid Infinity
// to fail the isNumber test (#7709).
translatedValue = clamp(translatedValue, -1e9, 1e9);
x1 = x2 = translatedValue + transB;
y1 = y2 = cHeight - translatedValue - transB;
if (!isNumber(translatedValue)) { // No min or max
skip = true;
force = false; // #7175, don't force it when path is invalid
}
else if (axis.horiz) {
y1 = axisTop;
y2 = cHeight - axis.bottom + (axis.options.isInternal ?
0 :
(chart.scrollablePixelsY || 0)); // #20354, scrollablePixelsY shouldn't be used for navigator
x1 = x2 = between(x1, axisLeft, axisLeft + axis.width);
}
else {
x1 = axisLeft;
x2 = cWidth - axis.right + (chart.scrollablePixelsX || 0);
y1 = y2 = between(y1, axisTop, axisTop + axis.height);
}
e.path = skip && !force ?
void 0 :
chart.renderer.crispLine([['M', x1, y1], ['L', x2, y2]], lineWidth || 1);
});
return evt.path;
}
/**
* Internal function to get the tick positions of a linear axis to round
* values like whole tens or every five.
*
* @function Highcharts.Axis#getLinearTickPositions
*
* @param {number} tickInterval
* The normalized tick interval.
*
* @param {number} min
* Axis minimum.
*
* @param {number} max
* Axis maximum.
*
* @return {Array<number>}
* An array of axis values where ticks should be placed.
*/
getLinearTickPositions(tickInterval, min, max) {
const roundedMin = correctFloat(Math.floor(min / tickInterval) * tickInterval), roundedMax = correctFloat(Math.ceil(max / tickInterval) * tickInterval), tickPositions = [];
let pos, lastPos, precision;
// When the precision is higher than what we filter out in
// correctFloat, skip it (#6183).
if (correctFloat(roundedMin + tickInterval) === roundedMin) {
precision = 20;
}
// For single points, add a tick regardless of the relative position
// (#2662, #6274)
if (this.single) {
return [min];
}
// Populate the intermediate values
pos = roundedMin;
while (pos <= roundedMax) {
// Place the tick on the rounded value
tickPositions.push(pos);
// Always add the raw tickInterval, not the corrected one.
pos = correctFloat(pos + tickInterval, precision);
// If the interval is not big enough in the current min - max range
// to actually increase the loop variable, we need to break out to
// prevent endless loop. Issue #619
if (pos === lastPos) {
break;
}
// Record the last value
lastPos = pos;
}
return tickPositions;
}
/**
* Resolve the new minorTicks/minorTickInterval options into the legacy
* loosely typed minorTickInterval option.
*
* @function Highcharts.Axis#getMinorTickInterval
*
* @return {number|"auto"|null}
* Legacy option
*/
getMinorTickInterval() {
const { minorTicks, minorTickInterval } = this.options;
if (minorTicks === true) {
return pick(minorTickInterval, 'auto');
}
if (minorTicks === false) {
return;
}
return minorTickInterval;
}
/**
* Internal function to return the minor tick positions. For logarithmic
* axes, the same logic as for major ticks is reused.
*
* @function Highcharts.Axis#getMinorTickPositions
*
* @return {Array<number>}
* An array of axis values where ticks should be placed.
*/
getMinorTickPositions() {
const axis = this, options = axis.options, tickPositions = axis.tickPositions, minorTickInterval = axis.minorTickInterval, pointRangePadding = axis.pointRangePadding || 0, min = (axis.min || 0) - pointRangePadding, // #1498
max = (axis.max || 0) + pointRangePadding, // #1498
range = axis.brokenAxis?.hasBreaks ?
axis.brokenAxis.unitLength : max - min;
let minorTickPositions = [], pos;
// If minor ticks get too dense, they are hard to read, and may cause
// long running script. So we don't draw them.
if (range && range / minorTickInterval < axis.len / 3) { // #3875
const logarithmic = axis.logarithmic;
if (logarithmic) {
// For each interval in the major ticks, compute the minor ticks
// separately.
this.paddedTicks.forEach(function (_pos, i, paddedTicks) {
if (i) {
minorTickPositions.push.apply(minorTickPositions, logarithmic.getLogTickPositions(minorTickInterval, paddedTicks[i - 1], paddedTicks[i], true));
}
});
}
else if (axis.dateTime &&
this.getMinorTickInterval() === 'auto') { // #1314
minorTickPositions = minorTickPositions.concat(axis.getTimeTicks(axis.dateTime.normalizeTimeTickInterval(minorTickInterval), min, max, options.startOfWeek));
}
else {
for (pos = min + (tickPositions[0] - min) % minorTickInterval; pos <= max; pos += minorTickInterval) {
// Very, very, tight grid lines (#5771)
if (pos === minorTickPositions[0]) {
break;
}
minorTickPositions.push(pos);
}
}
}
if (minorTickPositions.length !== 0) {
axis.trimTicks(minorTickPositions); // #3652 #3743 #1498 #6330
}
return minorTickPositions;
}
/**
* Adjust the min and max for the minimum range. Keep in mind that the
* series data is not yet processed, so we don't have information on data
* cropping and grouping, or updated `axis.pointRange` or
* `series.pointRange`. The data can't be processed until we have finally
* established min and max.
*
* @private
* @function Highcharts.Axis#adjustForMinRange
*/
adjustForMinRange() {
const axis = this, options = axis.options, logarithmic = axis.logarithmic, time = axis.chart.time;
let { max, min, minRange } = axis, zoomOffset, spaceAvailable, closestDataRange, minArgs, maxArgs;
// Set the automatic minimum range based on the closest point distance
if (axis.isXAxis &&
typeof minRange === 'undefined' &&
!logarithmic) {
if (defined(options.min) ||
defined(options.max) ||
defined(options.floor) ||
defined(options.ceiling)) {
// Setting it to null, as opposed to undefined, signals we don't
// run this block again as per the condition above.
minRange = null;
}
else {
// Find the closest distance between raw data points, as opposed
// to closestPointRange that applies to processed points
// (cropped and grouped)
closestDataRange = getClosestDistance(axis.series.map((s) => {
const xData = s.getColumn('x');
// If xIncrement, we only need to measure the two first
// points to get the distance. Saves processing time.
return s.xIncrement ? xData.slice(0, 2) : xData;
})) || 0;
minRange = Math.min(closestDataRange * 5, axis.dataMax - axis.dataMin);
}
}
// If minRange is exceeded, adjust
if (isNumber(max) &&
isNumber(min) &&
isNumber(minRange) &&
max - min < minRange) {
spaceAvailable =
axis.dataMax - axis.dataMin >=
minRange;
zoomOffset = (minRange - max + min) / 2;
// If min and max options have been set, don't go beyond it
minArgs = [
min - zoomOffset,
time.parse(options.min) ?? (min - zoomOffset)
];
// If space is available, stay within the data range
if (spaceAvailable) {
minArgs[2] = logarithmic ?
logarithmic.log2lin(axis.dataMin) :
axis.dataMin;
}
min = arrayMax(minArgs);
maxArgs = [
min + minRange,
time.parse(options.max) ?? (min + minRange)
];
// If space is available, stay within the data range
if (spaceAvailable) {
maxArgs[2] = logarithmic ?
logarithmic.log2lin(axis.dataMax) :
axis.dataMax;
}
max = arrayMin(maxArgs);
// Now if the max is adjusted, adjust the min back
if (max - min < minRange) {
minArgs[0] = max - minRange;
minArgs[1] = time.parse(options.min) ?? (max - minRange);
min = arrayMax(minArgs);
}
}
// Record modified extremes
axis.minRange = minRange;
axis.min = min;
axis.max = max;
}
/**
* Find the closestPointRange across all series, including the single data
* series.
*
* @private
* @function Highcharts.Axis#getClosest
*/
getClosest() {
let closestSingleDistance, closestDistance;
if (this.categories) {
closestDistance = 1;
}
else {
const singleXs = [];
this.series.forEach(function (series) {
const seriesClosest = series.closestPointRange, xData = series.getColumn('x');
if (xData.length === 1) {
singleXs.push(xData[0]);
}
else if (series.sorted &&
defined(seriesClosest) &&
series.reserveSpace()) {
closestDistance = defined(closestDistance) ?
Math.min(closestDistance, seriesClosest) :
seriesClosest;
}
});
if (singleXs.length) {
singleXs.sort((a, b) => a - b);
closestSingleDistance = getClosestDistance([singleXs]);
}
}
if (closestSingleDistance && closestDistance) {
return Math.min(closestSingleDistance, closestDistance);
}
return closestSingleDistance || closestDistance;
}
/**
* When a point name is given and no x, search for the name in the existing
* categories, or if categories aren't provided, search names or create a
* new category (#2522).
*
* @private
* @function Highcharts.Axis#nameToX
*
* @param {Highcharts.Point} point
* The point to inspect.
*
* @return {number}
* The X value that the point is given.
*/
nameToX(point) {
const explicitCategories = isArray(this.options.categories), names = explicitCategories ? this.categories : this.names;
let nameX = point.options.x, x;
point.series.requireSorting = false;
if (!defined(nameX)) {
nameX = this.uniqueNames && names ?
(explicitCategories ?
names.indexOf(point.name) :
pick(names.keys[point.name], -1)) :
point.series.autoIncrement();
}
if (nameX === -1) { // Not found in current categories
if (!explicitCategories && names) {
x = names.length;
}
}
else if (isNumber(nameX)) {
x = nameX;
}
// Write the last point's name to the names array
if (typeof x !== 'undefined') {
this.names[x] = point.name;
// Backwards mapping is much faster than array searching (#7725)
this.names.keys[point.name] = x;
}
else if (point.x) {
x = point.x; // #17438
}
return x;
}
/**
* When changes have been done to series data, update the axis.names.
*
* @private
* @function Highcharts.Axis#updateNames
*/
updateNames() {
const axis = this, names = this.names, i = names.length;
if (i > 0) {
Object.keys(names.keys).forEach(function (key) {
delete (names.keys)[key];
});
names.length = 0;
this.minRange = this.userMinRange; // Reset
(this.series || []).forEach((series) => {
// Reset incrementer (#5928)
series.xIncrement = null;
// When adding a series, points are not yet generated
if (!series.points || series.isDirtyData) {
// When we're updating the series with data that is longer
// than it was, and cropThreshold is passed, we need to make
// sure that the axis.max is increased _before_ running the
// premature processData. Otherwise this early iteration of
// processData will crop the points to axis.max, and the
// names array will be too short (#5857).
axis.max = Math.max(axis.max || 0, series.dataTable.rowCount - 1);
series.processData();
series.generatePoints();
}
const xData = series.getColumn('x').slice();
series.data.forEach((point, i) => {
let x = xData[i];
if (point?.options &&
typeof point.name !== 'undefined' // #9562
) {
x = axis.nameToX(point);
if (typeof x !== 'undefined' && x !== point.x) {
xData[i] = point.x = x;
}
}
});
series.dataTable.setColumn('x', xData);
});
}
}
/**
* Update translation information.
*
* @private
* @function Highcharts.Axis#setAxisTranslation
*
* @emits Highcharts.Axis#event:afterSetAxisTranslation
*/
setAxisTranslation() {
const axis = this, range = axis.max - axis.min, linkedParent = axis.linkedParent, hasCategories = !!axis.categories, isXAxis = axis.isXAxis;
let pointRange = axis.axisPointRange || 0, closestPointRange, minPointOffset = 0, pointRangePadding = 0, ordinalCorrection, transA = axis.transA;
// Adjust translation for padding. Y axis with categories need to go
// through the same (#1784).
if (isXAxis || hasCategories || pointRange) {
// Get the closest points
closestPointRange = axis.getClosest();
if (linkedParent) {
minPointOffset = linkedParent.minPointOffset;
pointRangePadding = linkedParent.pointRangePadding;
}
else {
axis.series.forEach(function (series) {
const seriesPointRange = hasCategories ?
1 :
(isXAxis ?
pick(series.options.pointRange, closestPointRange, 0) :
(axis.axisPointRange || 0)), // #2806
pointPlacement = series.options.pointPlacement;
pointRange = Math.max(pointRange, seriesPointRange);
if (!axis.single || hasCategories) {
// TODO: series should internally set x- and y-
// pointPlacement to simplify this logic.
const isPointPlacementAxis = series.is('xrange') ?
!isXAxis :
isXAxis;
// The `minPointOffset` is the value padding to the left
// of the axis in order to make room for points with a
// pointRange, typically columns, or line/scatter points
// on a category axis. When the `pointPlacement` option
// is 'between' or 'on', this padding does not apply.
minPointOffset = Math.max(minPointOffset, isPointPlacementAxis && isString(pointPlacement) ?
0 :
seriesPointRange / 2);
// Determine the total padding needed to the length of
// the axis to make room for the pointRange. If the
// series' pointPlacement is 'on', no padding is added.
pointRangePadding = Math.max(pointRangePadding, isPointPlacementAxis && pointPlacement === 'on' ?
0 :
seriesPointRange);
}
});
}
// Record minPointOffset and pointRangePadding
ordinalCorrection = (axis.ordinal?.slope && closestPointRange) ?
axis.ordinal.slope / closestPointRange :
1; // #988, #1853
axis.minPointOffset = minPointOffset =
minPointOffset * ordinalCorrection;
axis.pointRangePadding =
pointRangePadding = pointRangePadding * ordinalCorrection;
// The `pointRange` is the width reserved for each point, like in a
// column chart
axis.pointRange = Math.min(pointRange, axis.single && hasCategories ? 1 : range);
// The `closestPointRange` is the closest distance between points.
// In columns it is mostly equal to pointRange, but in lines
// pointRange is 0 while closestPointRange is some other value
if (isXAxis) {
axis.closestPointRange = closestPointRange;
}
}
// Secondary values
axis.translationSlope = axis.transA = transA =
axis.staticScale ||
axis.len / ((range + pointRangePadding) || 1);
// Translation addend
axis.transB = axis.horiz ? axis.left : axis.bottom;
axis.minPixelPadding = transA * minPointOffset;
fireEvent(this, 'afterSetAxisTranslation');
}
/**
* @private
* @function Highcharts.Axis#minFromRange
*/
minFromRange() {
const { max, min } = this;
return isNumber(max) && isNumber(min) && max - min || void 0;
}
/**
* Set the tick positions to round values and optionally extend the extremes
* to the nearest tick.
*
* @private
* @function Highcharts.Axis#setTickInterval
*
* @param {boolean} secondPass
* TO-DO: parameter description
*
* @emits Highcharts.Axis#event:foundExtremes
*/
setTickInterval(secondPass) {
const axis = this, { categories, chart, dataMax, dataMin, dateTime, isXAxis, logarithmic, options, softThreshold } = axis, time = chart.time, threshold = isNumber(axis.threshold) ? axis.threshold : void 0, minRange = axis.minRange || 0, { ceiling, floor, linkedTo, softMax, softMin } = options, linkedParent = isNumber(linkedTo) && chart[axis.coll]?.[linkedTo], tickPixelIntervalOption = options.tickPixelInterval;
let maxPadding = options.maxPadding, minPadding = options.minPadding, range = 0, linkedParentExtremes,
// Only non-negative tickInterval is valid, #12961
tickIntervalOption = isNumber(options.tickInterval) && options.tickInterval >= 0 ?
options.tickInterval : void 0, thresholdMin, thresholdMax, hardMin, hardMax;
if (!dateTime && !categories && !linkedParent) {
this.getTickAmount();
}
// Min or max set either by zooming/setExtremes or initial options
hardMin = pick(axis.userMin, time.parse(options.min));
hardMax = pick(axis.userMax, time.parse(options.max));
// Linked axis gets the extremes from the parent axis
if (linkedParent) {
axis.linkedParent = linkedParent;
linkedParentExtremes = linkedParent.getExtremes();
axis.min = pick(linkedParentExtremes.min, linkedParentExtremes.dataMin);
axis.max = pick(linkedParentExtremes.max, linkedParentExtremes.dataMax);
if (this.type !== linkedParent.type) {
// Can't link axes of different type
error(11, true, chart);
}
// Initial min and max from the extreme data values
}
else {
// Adjust to hard threshold
if (softThreshold &&
defined(threshold) &&
isNumber(dataMax) &&
isNumber(dataMin)) {
if (dataMin >= threshold) {
thresholdMin = threshold;
minPadding = 0;
}
else if (dataMax <= threshold) {
thresholdMax = threshold;
maxPadding = 0;
}
}
axis.min = pick(hardMin, thresholdMin, dataMin);
axis.max = pick(hardMax, thresholdMax, dataMax);
}
if (isNumber(axis.max) && isNumber(axis.min)) {
if (logarithmic) {
if (axis.positiveValuesOnly &&
!secondPass &&
Math.min(axis.min, pick(dataMin, axis.min)) <= 0) { // #978
// Can't plot negative values on log axis
error(10, true, chart);
}
// The correctFloat cures #934, float errors on full tens. But
// it was too aggressive for #4360 because of conversion back to
// lin, therefore use precision 15.
axis.min = correctFloat(logarithmic.log2lin(axis.min), 16);
axis.max = correctFloat(logarithmic.log2lin(axis.max), 16);
}
// Handle zoomed range
if (axis.range && isNumber(dataMin)) {
// #618, #6773:
axis.userMin = axis.min = hardMin = Math.max(dataMin, axis.minFromRange() || 0);
axis.userMax = hardMax = axis.max;
axis.range = void 0; // Don't use it when running setExtremes
}
}
// Hook for Highcharts Stock Scroller and bubble axis padding
fireEvent(axis, 'foundExtremes');
// Adjust min and max for the minimum range
axis.adjustForMinRange();
if (isNumber(axis.min) && isNumber(axis.max)) {
// Handle options for floor, ceiling, softMin and softMax (#6359)
if (!isNumber(axis.userMin) &&
isNumber(softMin) &&
softMin < axis.min) {
axis.min = hardMin = softMin; // #6894
}
if (!isNumber(axis.userMax) &&
isNumber(softMax) &&
softMax > axis.max) {
axis.max = hardMax = softMax; // #6894
}
// Pad the values to get clear of the chart's edges. To avoid
// tickInterval taking the padding into account, we do this after
// computing tick interval (#1337).
if (!categories &&
!axis.axisPointRange &&
!axis.stacking?.usePercentage &&
!linkedParent) {
range = axis.max - axis.min;
if (range) {
if (!defined(hardMin) && minPadding) {
axis.min -= range * minPadding;
}
if (!defined(hardMax) && maxPadding) {
axis.max += range * maxPadding;
}
}
}
if (!isNumber(axis.userMin) && isNumber(floor)) {
axis.min = Math.max(axis.min, floor);
}
if (!isNumber(axis.userMax) && isNumber(ceiling)) {
axis.max = Math.min(axis.max, ceiling);
}
// When the threshold is soft, adjust the extreme value only if the
// data extreme and the padded extreme land on either side of the
// threshold. For example, a series of [0, 1, 2, 3] would make the
// yAxis add a tick for -1 because of the default `minPadding` and
// `startOnTick` options. This is prevented by the `softThreshold`
// option.
if (softThreshold &&
isNumber(dataMin) &&
isNumber(dataMax)) {
const numThreshold = threshold || 0;
if (!defined(hardMin) &&
axis.min < numThreshold &&
dataMin >= numThreshold) {
axis.min = options.minRange ?
Math.min(numThreshold, axis.max - minRange) :
numThreshold;
}
else if (!