highcharts
Version:
JavaScript charting framework
1,036 lines (1,035 loc) • 47.5 kB
JavaScript
/* *
*
* (c) 2010-2025 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
;
import Axis from './Axis.js';
import DataTableCore from '../../Data/DataTableCore.js';
import H from '../Globals.js';
import U from '../Utilities.js';
const { addEvent, correctFloat, css, defined, error, isNumber, pick, timeUnits, isString } = U;
/* *
*
* Composition
*
* */
/**
* Extends the axis with ordinal support.
* @private
*/
var OrdinalAxis;
(function (OrdinalAxis) {
/* *
*
* Declarations
*
* */
/* *
*
* Functions
*
* */
/**
* Extends the axis with ordinal support.
*
* @private
*
* @param AxisClass
* Axis class to extend.
*
* @param ChartClass
* Chart class to use.
*
* @param SeriesClass
* Series class to use.
*/
function compose(AxisClass, SeriesClass, ChartClass) {
const axisProto = AxisClass.prototype;
if (!axisProto.ordinal2lin) {
axisProto.getTimeTicks = getTimeTicks;
axisProto.index2val = index2val;
axisProto.lin2val = lin2val;
axisProto.val2lin = val2lin;
// Record this to prevent overwriting by broken-axis module (#5979)
axisProto.ordinal2lin = axisProto.val2lin;
addEvent(AxisClass, 'afterInit', onAxisAfterInit);
addEvent(AxisClass, 'foundExtremes', onAxisFoundExtremes);
addEvent(AxisClass, 'afterSetScale', onAxisAfterSetScale);
addEvent(AxisClass, 'initialAxisTranslation', onAxisInitialAxisTranslation);
addEvent(ChartClass, 'pan', onChartPan);
addEvent(ChartClass, 'touchpan', onChartPan);
addEvent(SeriesClass, 'updatedData', onSeriesUpdatedData);
}
return AxisClass;
}
OrdinalAxis.compose = compose;
/**
* In an ordinal axis, there might be areas with dense concentrations of
* points, then large gaps between some. Creating equally distributed
* ticks over this entire range may lead to a huge number of ticks that
* will later be removed. So instead, break the positions up in
* segments, find the tick positions for each segment then concatenize
* them. This method is used from both data grouping logic and X axis
* tick position logic.
* @private
*/
function getTimeTicks(normalizedInterval, min, max, startOfWeek, positions = [], closestDistance = 0, findHigherRanks) {
const higherRanks = {}, tickPixelIntervalOption = this.options.tickPixelInterval, time = this.chart.time,
// Record all the start positions of a segment, to use when
// deciding what's a gap in the data.
segmentStarts = [];
let end, segmentPositions, hasCrossedHigherRank, info, outsideMax, start = 0, groupPositions = [], lastGroupPosition = -Number.MAX_VALUE;
// The positions are not always defined, for example for ordinal
// positions when data has regular interval (#1557, #2090)
if ((!this.options.ordinal && !this.options.breaks) ||
!positions ||
positions.length < 3 ||
typeof min === 'undefined') {
return time.getTimeTicks.apply(time, arguments);
}
// Analyze the positions array to split it into segments on gaps
// larger than 5 times the closest distance. The closest distance is
// already found at this point, so we reuse that instead of
// computing it again.
const posLength = positions.length;
for (end = 0; end < posLength; end++) {
outsideMax = end && positions[end - 1] > max;
if (positions[end] < min) { // Set the last position before min
start = end;
}
if (end === posLength - 1 ||
positions[end + 1] - positions[end] > closestDistance * 5 ||
outsideMax) {
// For each segment, calculate the tick positions from the
// getTimeTicks utility function. The interval will be the
// same regardless of how long the segment is.
if (positions[end] > lastGroupPosition) { // #1475
segmentPositions = time.getTimeTicks(normalizedInterval, positions[start], positions[end], startOfWeek);
// Prevent duplicate groups, for example for multiple
// segments within one larger time frame (#1475)
while (segmentPositions.length &&
segmentPositions[0] <= lastGroupPosition) {
segmentPositions.shift();
}
if (segmentPositions.length) {
lastGroupPosition =
segmentPositions[segmentPositions.length - 1];
}
segmentStarts.push(groupPositions.length);
groupPositions = groupPositions.concat(segmentPositions);
}
// Set start of next segment
start = end + 1;
}
if (outsideMax) {
break;
}
}
// Get the grouping info from the last of the segments. The info is
// the same for all segments.
if (segmentPositions) {
info = segmentPositions.info;
// Optionally identify ticks with higher rank, for example
// when the ticks have crossed midnight.
if (findHigherRanks && info.unitRange <= timeUnits.hour) {
end = groupPositions.length - 1;
// Compare points two by two
for (start = 1; start < end; start++) {
if (time.dateFormat('%d', groupPositions[start]) !==
time.dateFormat('%d', groupPositions[start - 1])) {
higherRanks[groupPositions[start]] = 'day';
hasCrossedHigherRank = true;
}
}
// If the complete array has crossed midnight, we want
// to mark the first positions also as higher rank
if (hasCrossedHigherRank) {
higherRanks[groupPositions[0]] = 'day';
}
info.higherRanks = higherRanks;
}
// Save the info
info.segmentStarts = segmentStarts;
groupPositions.info = info;
}
else {
error(12, false, this.chart);
}
// Don't show ticks within a gap in the ordinal axis, where the
// space between two points is greater than a portion of the tick
// pixel interval
if (findHigherRanks && defined(tickPixelIntervalOption)) {
const length = groupPositions.length, translatedArr = [], distances = [];
let itemToRemove, translated, lastTranslated, medianDistance, distance, i = length;
// Find median pixel distance in order to keep a reasonably even
// distance between ticks (#748)
while (i--) {
translated = this.translate(groupPositions[i]);
if (lastTranslated) {
distances[i] = lastTranslated - translated;
}
translatedArr[i] = lastTranslated = translated;
}
distances.sort((a, b) => a - b);
medianDistance = distances[Math.floor(distances.length / 2)];
if (medianDistance < tickPixelIntervalOption * 0.6) {
medianDistance = null;
}
// Now loop over again and remove ticks where needed
i = groupPositions[length - 1] > max ? length - 1 : length; // #817
lastTranslated = void 0;
while (i--) {
translated = translatedArr[i];
distance = Math.abs(lastTranslated - translated);
// #4175 - when axis is reversed, the distance, is negative but
// tickPixelIntervalOption positive, so we need to compare the
// same values
// Remove ticks that are closer than 0.6 times the pixel
// interval from the one to the right, but not if it is close to
// the median distance (#748).
if (lastTranslated &&
distance < tickPixelIntervalOption * 0.8 &&
(medianDistance === null || distance < medianDistance * 0.8)) {
// Is this a higher ranked position with a normal
// position to the right?
if (higherRanks[groupPositions[i]] &&
!higherRanks[groupPositions[i + 1]]) {
// Yes: remove the lower ranked neighbour to the
// right
itemToRemove = i + 1;
lastTranslated = translated; // #709
}
else {
// No: remove this one
itemToRemove = i;
}
groupPositions.splice(itemToRemove, 1);
}
else {
lastTranslated = translated;
}
}
}
return groupPositions;
}
/**
* Get axis position of given index of the extended ordinal positions.
* Used only when panning an ordinal axis.
*
* @private
* @function Highcharts.Axis#index2val
* @param {number} index
* The index value of searched point
*/
function index2val(index) {
const axis = this, ordinal = axis.ordinal,
// Context could be changed to extendedOrdinalPositions.
ordinalPositions = ordinal.positions;
// The visible range contains only equally spaced values.
if (!ordinalPositions) {
return index;
}
let i = ordinalPositions.length - 1, distance;
if (index < 0) { // Out of range, in effect panning to the left
index = ordinalPositions[0];
}
else if (index > i) { // Out of range, panning to the right
index = ordinalPositions[i];
}
else { // Split it up
i = Math.floor(index);
distance = index - i; // The decimal
}
if (typeof distance !== 'undefined' &&
typeof ordinalPositions[i] !== 'undefined') {
return ordinalPositions[i] + (distance ?
distance *
(ordinalPositions[i + 1] - ordinalPositions[i]) :
0);
}
return index;
}
/**
* Translate from linear (internal) to axis value.
*
* @private
* @function Highcharts.Axis#lin2val
* @param {number} val
* The linear abstracted value.
*/
function lin2val(val) {
const axis = this, ordinal = axis.ordinal, localMin = axis.old ? axis.old.min : axis.min, localA = axis.old ? axis.old.transA : axis.transA;
// Always use extendedPositions (#19816)
const positions = ordinal.getExtendedPositions();
// In some cases (especially in early stages of the chart creation) the
// getExtendedPositions might return undefined.
if (positions?.length) {
// Convert back from modivied value to pixels. // #15970
const pixelVal = correctFloat((val - localMin) * localA +
axis.minPixelPadding), index = correctFloat(ordinal.getIndexOfPoint(pixelVal, positions)), mantissa = correctFloat(index % 1);
// Check if the index is inside position array. If true,
// read/approximate value for that exact index.
if (index >= 0 && index <= positions.length - 1) {
const leftNeighbour = positions[Math.floor(index)], rightNeighbour = positions[Math.ceil(index)], distance = rightNeighbour - leftNeighbour;
return positions[Math.floor(index)] + mantissa * distance;
}
}
// If the value is outside positions array, return initial value
return val; // #16784
}
/**
* Internal function to calculate the precise index in ordinalPositions
* array.
* @private
*/
function getIndexInArray(ordinalPositions, val) {
const index = OrdinalAxis.Additions.findIndexOf(ordinalPositions, val, true);
if (ordinalPositions[index] === val) {
return index;
}
const percent = (val - ordinalPositions[index]) /
(ordinalPositions[index + 1] - ordinalPositions[index]);
return index + percent;
}
/**
* @private
*/
function onAxisAfterInit() {
const axis = this;
if (!axis.ordinal) {
axis.ordinal = new OrdinalAxis.Additions(axis);
}
}
/**
* @private
*/
function onAxisFoundExtremes() {
const axis = this, { eventArgs, options } = axis;
if (axis.isXAxis &&
defined(options.overscroll) &&
options.overscroll !== 0 &&
isNumber(axis.max) &&
isNumber(axis.min)) {
if (axis.options.ordinal && !axis.ordinal.originalOrdinalRange) {
// Calculate the original ordinal range
axis.ordinal.getExtendedPositions(false);
}
if (axis.max === axis.dataMax &&
(
// Panning is an exception. We don't want to apply
// overscroll when panning over the dataMax
eventArgs?.trigger !== 'pan' ||
axis.isInternal) &&
// Scrollbar buttons are the other execption
eventArgs?.trigger !== 'navigator') {
const overscroll = axis.ordinal.convertOverscroll(options.overscroll);
axis.max += overscroll;
// Live data and buttons require translation for the min:
if (!axis.isInternal &&
defined(axis.userMin) &&
eventArgs?.trigger !== 'mousewheel') {
axis.min += overscroll;
}
}
}
}
/**
* For ordinal axis, that loads data async, redraw axis after data is
* loaded. If we don't do that, axis will have the same extremes as
* previously, but ordinal positions won't be calculated. See #10290
* @private
*/
function onAxisAfterSetScale() {
const axis = this;
if (axis.horiz && !axis.isDirty) {
axis.isDirty = axis.isOrdinal &&
axis.chart.navigator &&
!axis.chart.navigator.adaptToUpdatedData;
}
}
/**
* @private
*/
function onAxisInitialAxisTranslation() {
const axis = this;
if (axis.ordinal) {
axis.ordinal.beforeSetTickPositions();
axis.tickInterval = axis.ordinal.postProcessTickInterval(axis.tickInterval);
}
}
/**
* Extending the Chart.pan method for ordinal axes
* @private
*/
function onChartPan(e) {
const chart = this, xAxis = chart.xAxis[0], overscroll = xAxis.ordinal.convertOverscroll(xAxis.options.overscroll), chartX = e.originalEvent.chartX, panning = chart.options.chart.panning;
let runBase = false;
if (panning &&
panning.type !== 'y' &&
xAxis.options.ordinal &&
xAxis.series.length &&
// On touch devices, let default function handle the pinching
(!e.touches || e.touches.length <= 1)) {
const mouseDownX = chart.mouseDownX, extremes = xAxis.getExtremes(), dataMin = extremes.dataMin, dataMax = extremes.dataMax, min = extremes.min, max = extremes.max, hoverPoints = chart.hoverPoints, closestPointRange = (xAxis.closestPointRange ||
xAxis.ordinal?.overscrollPointsRange), pointPixelWidth = (xAxis.translationSlope *
(xAxis.ordinal.slope || closestPointRange)),
// How many ordinal units did we move?
movedUnits = Math.round((mouseDownX - chartX) / pointPixelWidth),
// Get index of all the chart's points
extendedOrdinalPositions = xAxis.ordinal.getExtendedPositions(), extendedAxis = {
ordinal: {
positions: extendedOrdinalPositions,
extendedOrdinalPositions: extendedOrdinalPositions
}
}, index2val = xAxis.index2val, val2lin = xAxis.val2lin;
let trimmedRange, ordinalPositions;
// Make sure panning to the edges does not decrease the zoomed range
if ((min <= dataMin && movedUnits < 0) ||
(max + overscroll >= dataMax && movedUnits > 0)) {
return;
}
// We have an ordinal axis, but the data is equally spaced
if (!extendedAxis.ordinal.positions) {
runBase = true;
}
else if (Math.abs(movedUnits) > 1) {
// Remove active points for shared tooltip
if (hoverPoints) {
hoverPoints.forEach(function (point) {
point.setState();
});
}
// In grouped data series, the last ordinal position represents
// the grouped data, which is to the left of the real data max.
// If we don't compensate for this, we will be allowed to pan
// grouped data series passed the right of the plot area.
ordinalPositions = extendedAxis.ordinal.positions;
if (dataMax >
ordinalPositions[ordinalPositions.length - 1]) {
ordinalPositions.push(dataMax);
}
// Get the new min and max values by getting the ordinal index
// for the current extreme, then add the moved units and
// translate back to values. This happens on the extended
// ordinal positions if the new position is out of range, else
// it happens on the current x axis which is smaller and faster.
chart.setFixedRange(max - min);
trimmedRange = xAxis.navigatorAxis
.toFixedRange(void 0, void 0, index2val.apply(extendedAxis, [
val2lin.apply(extendedAxis, [min, true]) +
movedUnits
]), index2val.apply(extendedAxis, [
val2lin.apply(extendedAxis, [max, true]) +
movedUnits
]));
// Apply it if it is within the available data range
if (trimmedRange.min >= Math.min(ordinalPositions[0], min) &&
trimmedRange.max <= Math.max(ordinalPositions[ordinalPositions.length - 1], max) + overscroll) {
xAxis.setExtremes(trimmedRange.min, trimmedRange.max, true, false, { trigger: 'pan' });
}
chart.mouseDownX = chartX; // Set new reference for next run
css(chart.container, { cursor: 'move' });
}
}
else {
runBase = true;
}
// Revert to the linear chart.pan version
if (runBase || (panning && /y/.test(panning.type))) {
if (overscroll) {
xAxis.max = xAxis.dataMax + overscroll;
}
}
else {
e.preventDefault();
}
}
/**
* @private
*/
function onSeriesUpdatedData() {
const xAxis = this.xAxis;
// Destroy the extended ordinal index on updated data
// and destroy extendedOrdinalPositions, #16055.
if (xAxis?.options.ordinal) {
delete xAxis.ordinal.index;
delete xAxis.ordinal.originalOrdinalRange;
}
}
/**
* Translate from a linear axis value to the corresponding ordinal axis
* position. If there are no gaps in the ordinal axis this will be the
* same. The translated value is the value that the point would have if
* the axis was linear, using the same min and max.
*
* @private
* @function Highcharts.Axis#val2lin
* @param {number} val
* The axis value.
* @param {boolean} [toIndex]
* Whether to return the index in the ordinalPositions or the new value.
*/
function val2lin(val, toIndex) {
const axis = this, ordinal = axis.ordinal, ordinalPositions = ordinal.positions;
let slope = ordinal.slope, extendedOrdinalPositions;
if (!ordinalPositions) {
return val;
}
const ordinalLength = ordinalPositions.length;
let ordinalIndex;
// If the searched value is inside visible plotArea, ivastigate the
// value basing on ordinalPositions.
if (ordinalPositions[0] <= val &&
ordinalPositions[ordinalLength - 1] >= val) {
ordinalIndex = getIndexInArray(ordinalPositions, val);
// Final return value is based on ordinalIndex
}
else {
extendedOrdinalPositions = ordinal.getExtendedPositions?.();
if (!extendedOrdinalPositions?.length) {
return val;
}
const length = extendedOrdinalPositions.length;
if (!slope) {
slope =
(extendedOrdinalPositions[length - 1] -
extendedOrdinalPositions[0]) /
length;
}
// `originalPointReference` is equal to the index of first point of
// ordinalPositions in extendedOrdinalPositions.
const originalPositionsReference = getIndexInArray(extendedOrdinalPositions, ordinalPositions[0]);
// If the searched value is outside the visiblePlotArea,
// check if it is inside extendedOrdinalPositions.
if (val >= extendedOrdinalPositions[0] &&
val <=
extendedOrdinalPositions[length - 1]) {
// Return Value
ordinalIndex = getIndexInArray(extendedOrdinalPositions, val) -
originalPositionsReference;
}
else {
if (!toIndex) {
// If the value is outside positions array,
// return initial value, #16784
return val;
}
// Since ordinal.slope is the average distance between 2
// points on visible plotArea, this can be used to calculate
// the approximate position of the point, which is outside
// the extendedOrdinalPositions.
if (val < extendedOrdinalPositions[0]) {
const diff = extendedOrdinalPositions[0] - val, approximateIndexOffset = diff / slope;
ordinalIndex =
-originalPositionsReference -
approximateIndexOffset;
}
else {
const diff = val -
extendedOrdinalPositions[length - 1], approximateIndexOffset = diff / slope;
ordinalIndex =
approximateIndexOffset +
length -
originalPositionsReference;
}
}
}
return toIndex ? ordinalIndex : slope * (ordinalIndex || 0) +
ordinal.offset;
}
/* *
*
* Classes
*
* */
/**
* @private
*/
class Additions {
/* *
*
* Constructors
*
* */
/**
* @private
*/
constructor(axis) {
this.index = {};
this.axis = axis;
}
/* *
*
* Functions
*
* */
/**
* Calculate the ordinal positions before tick positions are calculated.
* @private
*/
beforeSetTickPositions() {
const axis = this.axis, ordinal = axis.ordinal, extremes = axis.getExtremes(), min = extremes.min, max = extremes.max, hasBreaks = axis.brokenAxis?.hasBreaks, isOrdinal = axis.options.ordinal;
let len, uniqueOrdinalPositions, dist, minIndex, maxIndex, slope, i, ordinalPositions = [], overscrollPointsRange = Number.MAX_VALUE, useOrdinal = false, adjustOrdinalExtremesPoints = false, isBoosted = false;
// Apply the ordinal logic
if (isOrdinal || hasBreaks) { // #4167 YAxis is never ordinal ?
let distanceBetweenPoint = 0;
axis.series.forEach(function (series, i) {
const xData = series.getColumn('x', true);
uniqueOrdinalPositions = [];
// For an axis with multiple series, check if the distance
// between points is identical throughout all series.
if (i > 0 &&
series.options.id !== 'highcharts-navigator-series' &&
xData.length > 1) {
adjustOrdinalExtremesPoints = (distanceBetweenPoint !== xData[1] - xData[0]);
}
distanceBetweenPoint = xData[1] - xData[0];
if (series.boosted) {
isBoosted = series.boosted;
}
if (series.reserveSpace() &&
(series
.takeOrdinalPosition !== false || hasBreaks)) {
// Concatenate the processed X data into the existing
// positions, or the empty array
ordinalPositions = ordinalPositions.concat(xData);
len = ordinalPositions.length;
// Remove duplicates (#1588)
ordinalPositions.sort(function (a, b) {
// Without a custom function it is sorted as strings
return a - b;
});
overscrollPointsRange = Math.min(overscrollPointsRange, pick(
// Check for a single-point series:
series.closestPointRange, overscrollPointsRange));
if (len) {
i = 0;
while (i < len - 1) {
if (ordinalPositions[i] !==
ordinalPositions[i + 1]) {
uniqueOrdinalPositions.push(ordinalPositions[i + 1]);
}
i++;
}
// Check first item:
if (uniqueOrdinalPositions[0] !==
ordinalPositions[0]) {
uniqueOrdinalPositions.unshift(ordinalPositions[0]);
}
ordinalPositions = uniqueOrdinalPositions;
}
}
});
if (!axis.ordinal.originalOrdinalRange) {
// Calculate current originalOrdinalRange
axis.ordinal.originalOrdinalRange =
(ordinalPositions.length - 1) * overscrollPointsRange;
}
// If the distance between points is not identical throughout
// all series, remove the first and last ordinal position to
// avoid enabling ordinal logic when it is not needed, #17405.
// Only for boosted series because changes are negligible.
if (adjustOrdinalExtremesPoints && isBoosted) {
ordinalPositions.pop();
ordinalPositions.shift();
}
// Cache the length
len = ordinalPositions.length;
// Check if we really need the overhead of mapping axis data
// against the ordinal positions. If the series consist of
// evenly spaced data any way, we don't need any ordinal logic.
if (len > 2) { // Two points have equal distance by default
dist = ordinalPositions[1] - ordinalPositions[0];
i = len - 1;
while (i-- && !useOrdinal) {
if (ordinalPositions[i + 1] - ordinalPositions[i] !==
dist) {
useOrdinal = true;
}
}
// When zooming in on a week, prevent axis padding for
// weekends even though the data within the week is evenly
// spaced.
if (!axis.options.keepOrdinalPadding &&
(ordinalPositions[0] - min > dist ||
(max -
ordinalPositions[ordinalPositions.length - 1]) > dist)) {
useOrdinal = true;
}
}
else if (axis.options.overscroll) {
if (len === 2) {
// Exactly two points, distance for overscroll is fixed:
overscrollPointsRange =
ordinalPositions[1] - ordinalPositions[0];
}
else if (len === 1) {
// We have just one point, closest distance is unknown.
// Assume then it is last point and overscrolled range:
overscrollPointsRange = axis.ordinal.convertOverscroll(axis.options.overscroll);
ordinalPositions = [
ordinalPositions[0],
ordinalPositions[0] + overscrollPointsRange
];
}
else {
// In case of zooming in on overscrolled range, stick to
// the old range:
overscrollPointsRange = ordinal.overscrollPointsRange;
}
}
// Record the slope and offset to compute the linear values from
// the array index. Since the ordinal positions may exceed the
// current range, get the start and end positions within it
// (#719, #665b)
if (useOrdinal || axis.forceOrdinal) {
if (axis.options.overscroll) {
ordinal.overscrollPointsRange = overscrollPointsRange;
ordinalPositions = ordinalPositions.concat(ordinal.getOverscrollPositions());
}
// Register
ordinal.positions = ordinalPositions;
// This relies on the ordinalPositions being set. Use
// Math.max and Math.min to prevent padding on either sides
// of the data.
minIndex = axis.ordinal2lin(// #5979
Math.max(min, ordinalPositions[0]), true);
maxIndex = Math.max(axis.ordinal2lin(Math.min(max, ordinalPositions[ordinalPositions.length - 1]), true), 1); // #3339
// Set the slope and offset of the values compared to the
// indices in the ordinal positions.
ordinal.slope = slope =
(max - min) / (maxIndex - minIndex);
ordinal.offset = min - (minIndex * slope);
}
else {
ordinal.overscrollPointsRange = pick(axis.closestPointRange, ordinal.overscrollPointsRange);
ordinal.positions = axis.ordinal.slope = ordinal.offset =
void 0;
}
}
axis.isOrdinal = isOrdinal && useOrdinal; // #3818, #4196, #4926
ordinal.groupIntervalFactor = null; // Reset for next run
}
/**
* Faster way of using the Array.indexOf method.
* Works for sorted arrays only with unique values.
*
* @param {Array} sortedArray
* The sorted array inside which we are looking for.
* @param {number} key
* The key to being found.
* @param {boolean} indirectSearch
* In case of lack of the point in the array, should return
* value be equal to -1 or the closest smaller index.
* @private
*/
static findIndexOf(sortedArray, key, indirectSearch) {
let start = 0, end = sortedArray.length - 1, middle;
while (start < end) {
middle = Math.ceil((start + end) / 2);
// Key found as the middle element.
if (sortedArray[middle] <= key) {
// Continue searching to the right.
start = middle;
}
else {
// Continue searching to the left.
end = middle - 1;
}
}
if (sortedArray[start] === key) {
return start;
}
// Key could not be found.
return !indirectSearch ? -1 : start;
}
/**
* Get the ordinal positions for the entire data set. This is necessary
* in chart panning because we need to find out what points or data
* groups are available outside the visible range. When a panning
* operation starts, if an index for the given grouping does not exists,
* it is created and cached. This index is deleted on updated data, so
* it will be regenerated the next time a panning operation starts.
* @private
*/
getExtendedPositions(withOverscroll = true) {
const ordinal = this, axis = ordinal.axis, axisProto = axis.constructor.prototype, chart = axis.chart, key = axis.series.reduce((k, series) => {
const grouping = series.currentDataGrouping;
return (k +
(grouping ? grouping.count + grouping.unitName : 'raw'));
}, ''), overscroll = withOverscroll ?
axis.ordinal.convertOverscroll(axis.options.overscroll) : 0, extremes = axis.getExtremes();
let fakeAxis, fakeSeries = void 0, ordinalIndex = ordinal.index;
// If this is the first time, or the ordinal index is deleted by
// updatedData,
// create it.
if (!ordinalIndex) {
ordinalIndex = ordinal.index = {};
}
if (!ordinalIndex[key]) {
// Create a fake axis object where the extended ordinal
// positions are emulated
fakeAxis = {
series: [],
chart: chart,
forceOrdinal: false,
getExtremes: function () {
return {
min: extremes.dataMin,
max: extremes.dataMax + overscroll
};
},
applyGrouping: axisProto.applyGrouping,
getGroupPixelWidth: axisProto.getGroupPixelWidth,
getTimeTicks: axisProto.getTimeTicks,
options: {
ordinal: true
},
ordinal: {
getGroupIntervalFactor: this.getGroupIntervalFactor
},
ordinal2lin: axisProto.ordinal2lin, // #6276
getIndexOfPoint: axisProto.getIndexOfPoint,
val2lin: axisProto.val2lin // #2590
};
fakeAxis.ordinal.axis = fakeAxis;
// Add the fake series to hold the full data, then apply
// processData to it
axis.series.forEach((series) => {
fakeSeries = {
xAxis: fakeAxis,
chart: chart,
groupPixelWidth: series.groupPixelWidth,
destroyGroupedData: H.noop,
getColumn: series.getColumn,
applyGrouping: series.applyGrouping,
getProcessedData: series.getProcessedData,
reserveSpace: series.reserveSpace,
visible: series.visible
};
const xData = series.getColumn('x').concat(withOverscroll ?
ordinal.getOverscrollPositions() :
[]);
fakeSeries.dataTable = new DataTableCore({
columns: {
x: xData
}
});
fakeSeries.options = {
...series.options,
dataGrouping: series.currentDataGrouping ? {
firstAnchor: series.options.dataGrouping?.firstAnchor,
anchor: series.options.dataGrouping?.anchor,
lastAnchor: series.options.dataGrouping?.firstAnchor,
enabled: true,
forced: true,
approximation: 'open',
units: [[
series.currentDataGrouping.unitName,
[series.currentDataGrouping.count]
]]
} : {
enabled: false
}
};
fakeAxis.series.push(fakeSeries);
series.processData.apply(fakeSeries);
});
fakeAxis.applyGrouping({ hasExtremesChanged: true });
// Force to use the ordinal when points are evenly spaced (e.g.
// weeks), #3825.
if ((fakeSeries?.closestPointRange !==
fakeSeries?.basePointRange) &&
fakeSeries.currentDataGrouping) {
fakeAxis.forceOrdinal = true;
}
// Run beforeSetTickPositions to compute the ordinalPositions
axis.ordinal.beforeSetTickPositions.apply({ axis: fakeAxis });
if (!axis.ordinal.originalOrdinalRange &&
fakeAxis.ordinal.originalOrdinalRange) {
axis.ordinal.originalOrdinalRange =
fakeAxis.ordinal.originalOrdinalRange;
}
// Cache it
if (fakeAxis.ordinal.positions) {
ordinalIndex[key] = fakeAxis.ordinal.positions;
}
}
return ordinalIndex[key];
}
/**
* Find the factor to estimate how wide the plot area would have been if
* ordinal gaps were included. This value is used to compute an imagined
* plot width in order to establish the data grouping interval.
*
* A real world case is the intraday-candlestick example. Without this
* logic, it would show the correct data grouping when viewing a range
* within each day, but once moving the range to include the gap between
* two days, the interval would include the cut-away night hours and the
* data grouping would be wrong. So the below method tries to compensate
* by identifying the most common point interval, in this case days.
*
* An opposite case is presented in issue #718. We have a long array of
* daily data, then one point is appended one hour after the last point.
* We expect the data grouping not to change.
*
* In the future, if we find cases where this estimation doesn't work
* optimally, we might need to add a second pass to the data grouping
* logic, where we do another run with a greater interval if the number
* of data groups is more than a certain fraction of the desired group
* count.
* @private
*/
getGroupIntervalFactor(xMin, xMax, series) {
const ordinal = this, processedXData = series.getColumn('x', true), len = processedXData.length, distances = [];
let median, i, groupIntervalFactor = ordinal.groupIntervalFactor;
// Only do this computation for the first series, let the other
// inherit it (#2416)
if (!groupIntervalFactor) {
// Register all the distances in an array
for (i = 0; i < len - 1; i++) {
distances[i] = (processedXData[i + 1] -
processedXData[i]);
}
// Sort them and find the median
distances.sort(function (a, b) {
return a - b;
});
median = distances[Math.floor(len / 2)];
// Compensate for series that don't extend through the entire
// axis extent. #1675.
xMin = Math.max(xMin, processedXData[0]);
xMax = Math.min(xMax, processedXData[len - 1]);
ordinal.groupIntervalFactor = groupIntervalFactor =
(len * median) / (xMax - xMin);
}
// Return the factor needed for data grouping
return groupIntervalFactor;
}
/**
* Get index of point inside the ordinal positions array.
*
* @private
* @param {number} pixelVal
* The pixel value of a point.
*
* @param {Array<number>} [ordinalArray]
* An array of all points available on the axis for the given data set.
* Either ordinalPositions if the value is inside the plotArea or
* extendedOrdinalPositions if not.
*/
getIndexOfPoint(pixelVal, ordinalArray) {
const ordinal = this, axis = ordinal.axis, min = axis.min, minX = axis.minPixelPadding, indexOfMin = getIndexInArray(ordinalArray, min);
const ordinalPointPixelInterval = axis.translationSlope *
(ordinal.slope ||
axis.closestPointRange ||
ordinal.overscrollPointsRange);
const shiftIndex = correctFloat((pixelVal - minX) / ordinalPointPixelInterval);
return indexOfMin + shiftIndex;
}
/**
* Get ticks for an ordinal axis within a range where points don't
* exist. It is required when overscroll is enabled. We can't base on
* points, because we may not have any, so we use approximated
* pointRange and generate these ticks between Axis.dataMax,
* Axis.dataMax + Axis.overscroll evenly spaced. Used in panning and
* navigator scrolling.
* @private
*/
getOverscrollPositions() {
const ordinal = this, axis = ordinal.axis, extraRange = ordinal.convertOverscroll(axis.options.overscroll), distance = ordinal.overscrollPointsRange, positions = [];
let max = axis.dataMax;
if (defined(distance)) {
// Max + pointRange because we need to scroll to the last
while (max < axis.dataMax + extraRange) {
max += distance;
positions.push(max);
}
}
return positions;
}
/**
* Make the tick intervals closer because the ordinal gaps make the
* ticks spread out or cluster.
* @private
*/
postProcessTickInterval(tickInterval) {
// Problem: https://jsfiddle.net/highcharts/FQm4E/1/. This is a case
// where this algorithm doesn't work optimally. In this case, the
// tick labels are spread out per week, but all the gaps reside
// within weeks. So we have a situation where the labels are courser
// than the ordinal gaps, and thus the tick interval should not be
// altered.
const ordinal = this, axis = ordinal.axis, ordinalSlope = ordinal.slope, closestPointRange = axis.closestPointRange;
let ret;
if (ordinalSlope && closestPointRange) {
if (!axis.options.breaks) {
ret = (tickInterval /
(ordinalSlope / closestPointRange));
}
else {
ret = closestPointRange || tickInterval; // #7275
}
}
else {
ret = tickInterval;
}
return ret;
}
/**
* If overscroll is pixel or percentage value, convert it to axis range.
*
* @private
* @param {number | string} overscroll
* Overscroll value in axis range, pixels or percentage value.
* @return {number}
* Overscroll value in axis range.
*/
convertOverscroll(overscroll = 0) {
const ordinal = this, axis = ordinal.axis, calculateOverscroll = function (overscrollPercentage) {
return pick(ordinal.originalOrdinalRange, defined(axis.dataMax) && defined(axis.dataMin) ?
axis.dataMax - axis.dataMin : 0) * overscrollPercentage;
};
if (isString(overscroll)) {
const overscrollValue = parseInt(overscroll, 10);
let isFullRange;
// #22334
if (defined(axis.min) && defined(axis.max) &&
defined(axis.dataMin) && defined(axis.dataMax)) {
isFullRange =
axis.max - axis.min === axis.dataMax - axis.dataMin;
if (!isFullRange) {
this.originalOrdinalRange = axis.max - axis.min;
}
}
if (/%$/.test(overscroll)) {
// If overscroll is percentage
return calculateOverscroll(overscrollValue / 100);
}
if (/px/.test(overscroll)) {
// If overscroll is pixels, it is limited to 90% of the axis
// length to prevent division by zero
const limitedOverscrollValue = Math.min(overscrollValue, axis.len * 0.9), pixelToPercent = limitedOverscrollValue / axis.len;
return calculateOverscroll(pixelToPercent /
(isFullRange ? (1 - pixelToPercent) : 1));
}
// If overscroll is a string but not pixels or percentage,
// return 0 as no overscroll
return 0;
}
return overscroll;
}
}
OrdinalAxis.Additions = Additions;
})(OrdinalAxis || (OrdinalAxis = {}));
/* *
*
* Default Export
*
* */
export default OrdinalAxis;