UNPKG

highcharts

Version:
1,199 lines 132 kB
/* * * * (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 (!