760 lines
36 KiB
JavaScript
760 lines
36 KiB
JavaScript
|
/* *
|
||
|
*
|
||
|
* (c) 2010-2020 Torstein Honsi
|
||
|
*
|
||
|
* License: www.highcharts.com/license
|
||
|
*
|
||
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
||
|
*
|
||
|
* */
|
||
|
'use strict';
|
||
|
import Axis from './Axis.js';
|
||
|
import H from './Globals.js';
|
||
|
import U from './Utilities.js';
|
||
|
var addEvent = U.addEvent, css = U.css, defined = U.defined, pick = U.pick, timeUnits = U.timeUnits;
|
||
|
import './Chart.js';
|
||
|
// Has a dependency on Navigator due to the use of Axis.toFixedRange
|
||
|
import './Navigator.js';
|
||
|
import './Series.js';
|
||
|
var Chart = H.Chart, Series = H.Series;
|
||
|
/* eslint-disable valid-jsdoc */
|
||
|
var OrdinalAxisAdditions = /** @class */ (function () {
|
||
|
/* *
|
||
|
*
|
||
|
* Constructors
|
||
|
*
|
||
|
* */
|
||
|
/**
|
||
|
* @private
|
||
|
*/
|
||
|
function OrdinalAxisAdditions(axis) {
|
||
|
this.index = {};
|
||
|
this.axis = axis;
|
||
|
}
|
||
|
/* *
|
||
|
*
|
||
|
* Functions
|
||
|
*
|
||
|
* */
|
||
|
/**
|
||
|
* 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
|
||
|
*/
|
||
|
OrdinalAxisAdditions.prototype.getExtendedPositions = function () {
|
||
|
var ordinal = this, axis = ordinal.axis, axisProto = axis.constructor.prototype, chart = axis.chart, grouping = axis.series[0].currentDataGrouping, ordinalIndex = ordinal.index, key = grouping ?
|
||
|
grouping.count + grouping.unitName :
|
||
|
'raw', overscroll = axis.options.overscroll, extremes = axis.getExtremes(), fakeAxis, fakeSeries;
|
||
|
// 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,
|
||
|
getExtremes: function () {
|
||
|
return {
|
||
|
min: extremes.dataMin,
|
||
|
max: extremes.dataMax + overscroll
|
||
|
};
|
||
|
},
|
||
|
options: {
|
||
|
ordinal: true
|
||
|
},
|
||
|
ordinal: {},
|
||
|
ordinal2lin: axisProto.ordinal2lin,
|
||
|
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(function (series) {
|
||
|
fakeSeries = {
|
||
|
xAxis: fakeAxis,
|
||
|
xData: series.xData.slice(),
|
||
|
chart: chart,
|
||
|
destroyGroupedData: H.noop,
|
||
|
getProcessedData: H.Series.prototype.getProcessedData
|
||
|
};
|
||
|
fakeSeries.xData = fakeSeries.xData.concat(ordinal.getOverscrollPositions());
|
||
|
fakeSeries.options = {
|
||
|
dataGrouping: grouping ? {
|
||
|
enabled: true,
|
||
|
forced: true,
|
||
|
// doesn't matter which, use the fastest
|
||
|
approximation: 'open',
|
||
|
units: [[
|
||
|
grouping.unitName,
|
||
|
[grouping.count]
|
||
|
]]
|
||
|
} : {
|
||
|
enabled: false
|
||
|
}
|
||
|
};
|
||
|
series.processData.apply(fakeSeries);
|
||
|
fakeAxis.series.push(fakeSeries);
|
||
|
});
|
||
|
// Run beforeSetTickPositions to compute the ordinalPositions
|
||
|
axis.beforeSetTickPositions.apply(fakeAxis);
|
||
|
// Cache it
|
||
|
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
|
||
|
*/
|
||
|
OrdinalAxisAdditions.prototype.getGroupIntervalFactor = function (xMin, xMax, series) {
|
||
|
var ordinal = this, axis = ordinal.axis, i, processedXData = series.processedXData, len = processedXData.length, distances = [], median, 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 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
|
||
|
*/
|
||
|
OrdinalAxisAdditions.prototype.getOverscrollPositions = function () {
|
||
|
var ordinal = this, axis = ordinal.axis, extraRange = axis.options.overscroll, distance = ordinal.overscrollPointsRange, positions = [], max = axis.dataMax;
|
||
|
if (defined(distance)) {
|
||
|
// Max + pointRange because we need to scroll to the last
|
||
|
positions.push(max);
|
||
|
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
|
||
|
*/
|
||
|
OrdinalAxisAdditions.prototype.postProcessTickInterval = function (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.
|
||
|
var ordinal = this, axis = ordinal.axis, ordinalSlope = ordinal.slope, ret;
|
||
|
if (ordinalSlope) {
|
||
|
if (!axis.options.breaks) {
|
||
|
ret = tickInterval / (ordinalSlope / axis.closestPointRange);
|
||
|
}
|
||
|
else {
|
||
|
ret = axis.closestPointRange || tickInterval; // #7275
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
ret = tickInterval;
|
||
|
}
|
||
|
return ret;
|
||
|
};
|
||
|
return OrdinalAxisAdditions;
|
||
|
}());
|
||
|
/**
|
||
|
* Extends the axis with ordinal support.
|
||
|
*
|
||
|
* @private
|
||
|
*/
|
||
|
var OrdinalAxis = /** @class */ (function () {
|
||
|
function OrdinalAxis() {
|
||
|
}
|
||
|
/**
|
||
|
* 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.
|
||
|
*/
|
||
|
OrdinalAxis.compose = function (AxisClass, ChartClass, SeriesClass) {
|
||
|
AxisClass.keepProps.push('ordinal');
|
||
|
var axisProto = AxisClass.prototype;
|
||
|
/**
|
||
|
* Calculate the ordinal positions before tick positions are calculated.
|
||
|
*
|
||
|
* @private
|
||
|
*/
|
||
|
axisProto.beforeSetTickPositions = function () {
|
||
|
var axis = this, ordinal = axis.ordinal, len, ordinalPositions = [], uniqueOrdinalPositions, useOrdinal = false, dist, extremes = axis.getExtremes(), min = extremes.min, max = extremes.max, minIndex, maxIndex, slope, hasBreaks = axis.isXAxis && !!axis.options.breaks, isOrdinal = axis.options.ordinal, overscrollPointsRange = Number.MAX_VALUE, ignoreHiddenSeries = axis.chart.options.chart.ignoreHiddenSeries, i, hasBoostedSeries;
|
||
|
// Apply the ordinal logic
|
||
|
if (isOrdinal || hasBreaks) { // #4167 YAxis is never ordinal ?
|
||
|
axis.series.forEach(function (series, i) {
|
||
|
uniqueOrdinalPositions = [];
|
||
|
if ((!ignoreHiddenSeries || series.visible !== false) &&
|
||
|
(series.takeOrdinalPosition !== false || hasBreaks)) {
|
||
|
// concatenate the processed X data into the existing
|
||
|
// positions, or the empty array
|
||
|
ordinalPositions = ordinalPositions.concat(series.processedXData);
|
||
|
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 (series.isSeriesBoosting) {
|
||
|
hasBoostedSeries = true;
|
||
|
}
|
||
|
});
|
||
|
if (hasBoostedSeries) {
|
||
|
ordinalPositions.length = 0;
|
||
|
}
|
||
|
// 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.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) {
|
||
|
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
|
||
|
};
|
||
|
/**
|
||
|
* In an ordinal axis, there might be areas with dense consentrations 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
|
||
|
*/
|
||
|
AxisClass.prototype.getTimeTicks = function (normalizedInterval, min, max, startOfWeek, positions, closestDistance, findHigherRanks) {
|
||
|
if (positions === void 0) { positions = []; }
|
||
|
if (closestDistance === void 0) { closestDistance = 0; }
|
||
|
var start = 0, end, segmentPositions, higherRanks = {}, hasCrossedHigherRank, info, posLength, outsideMax, groupPositions = [], lastGroupPosition = -Number.MAX_VALUE, 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 = [];
|
||
|
// 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.
|
||
|
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.
|
||
|
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;
|
||
|
// 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)) {
|
||
|
var length = groupPositions.length, i = length, itemToRemove, translated, translatedArr = [], lastTranslated, medianDistance, distance, distances = [];
|
||
|
// 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();
|
||
|
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;
|
||
|
};
|
||
|
/**
|
||
|
* Translate from linear (internal) to axis value.
|
||
|
*
|
||
|
* @private
|
||
|
* @function Highcharts.Axis#lin2val
|
||
|
*
|
||
|
* @param {number} val
|
||
|
* The linear abstracted value.
|
||
|
*
|
||
|
* @param {boolean} [fromIndex]
|
||
|
* Translate from an index in the ordinal positions rather than a
|
||
|
* value.
|
||
|
*
|
||
|
* @return {number}
|
||
|
*/
|
||
|
axisProto.lin2val = function (val, fromIndex) {
|
||
|
var axis = this, ordinal = axis.ordinal, ordinalPositions = ordinal.positions, ret;
|
||
|
// the visible range contains only equally spaced values
|
||
|
if (!ordinalPositions) {
|
||
|
ret = val;
|
||
|
}
|
||
|
else {
|
||
|
var ordinalSlope = ordinal.slope, ordinalOffset = ordinal.offset, i = ordinalPositions.length - 1, linearEquivalentLeft, linearEquivalentRight, distance;
|
||
|
// Handle the case where we translate from the index directly,
|
||
|
// used only when panning an ordinal axis
|
||
|
if (fromIndex) {
|
||
|
if (val < 0) { // out of range, in effect panning to the left
|
||
|
val = ordinalPositions[0];
|
||
|
}
|
||
|
else if (val > i) { // out of range, panning to the right
|
||
|
val = ordinalPositions[i];
|
||
|
}
|
||
|
else { // split it up
|
||
|
i = Math.floor(val);
|
||
|
distance = val - i; // the decimal
|
||
|
}
|
||
|
// Loop down along the ordinal positions. When the linear
|
||
|
// equivalent of i matches an ordinal position, interpolate
|
||
|
// between the left and right values.
|
||
|
}
|
||
|
else {
|
||
|
while (i--) {
|
||
|
linearEquivalentLeft =
|
||
|
(ordinalSlope * i) + ordinalOffset;
|
||
|
if (val >= linearEquivalentLeft) {
|
||
|
linearEquivalentRight =
|
||
|
(ordinalSlope *
|
||
|
(i + 1)) +
|
||
|
ordinalOffset;
|
||
|
// something between 0 and 1
|
||
|
distance = (val - linearEquivalentLeft) /
|
||
|
(linearEquivalentRight - linearEquivalentLeft);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
// If the index is within the range of the ordinal positions,
|
||
|
// return the associated or interpolated value. If not, just
|
||
|
// return the value.
|
||
|
return (typeof distance !== 'undefined' &&
|
||
|
typeof ordinalPositions[i] !== 'undefined' ?
|
||
|
ordinalPositions[i] + (distance ?
|
||
|
distance *
|
||
|
(ordinalPositions[i + 1] - ordinalPositions[i]) :
|
||
|
0) :
|
||
|
val);
|
||
|
}
|
||
|
return ret;
|
||
|
};
|
||
|
/**
|
||
|
* 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 were 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.
|
||
|
*
|
||
|
* @return {number}
|
||
|
*/
|
||
|
axisProto.val2lin = function (val, toIndex) {
|
||
|
var axis = this, ordinal = axis.ordinal, ordinalPositions = ordinal.positions, ret;
|
||
|
if (!ordinalPositions) {
|
||
|
ret = val;
|
||
|
}
|
||
|
else {
|
||
|
var ordinalLength = ordinalPositions.length, i, distance, ordinalIndex;
|
||
|
// first look for an exact match in the ordinalpositions array
|
||
|
i = ordinalLength;
|
||
|
while (i--) {
|
||
|
if (ordinalPositions[i] === val) {
|
||
|
ordinalIndex = i;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
// if that failed, find the intermediate position between the
|
||
|
// two nearest values
|
||
|
i = ordinalLength - 1;
|
||
|
while (i--) {
|
||
|
if (val > ordinalPositions[i] || i === 0) { // interpolate
|
||
|
// something between 0 and 1
|
||
|
distance = (val - ordinalPositions[i]) /
|
||
|
(ordinalPositions[i + 1] - ordinalPositions[i]);
|
||
|
ordinalIndex = i + distance;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
ret = toIndex ?
|
||
|
ordinalIndex :
|
||
|
ordinal.slope *
|
||
|
(ordinalIndex || 0) +
|
||
|
ordinal.offset;
|
||
|
}
|
||
|
return ret;
|
||
|
};
|
||
|
// Record this to prevent overwriting by broken-axis module (#5979)
|
||
|
axisProto.ordinal2lin = axisProto.val2lin;
|
||
|
/* eslint-disable no-invalid-this */
|
||
|
addEvent(AxisClass, 'afterInit', function () {
|
||
|
var axis = this;
|
||
|
if (!axis.ordinal) {
|
||
|
axis.ordinal = new OrdinalAxisAdditions(axis);
|
||
|
}
|
||
|
});
|
||
|
addEvent(AxisClass, 'foundExtremes', function () {
|
||
|
var axis = this;
|
||
|
if (axis.isXAxis &&
|
||
|
defined(axis.options.overscroll) &&
|
||
|
axis.max === axis.dataMax &&
|
||
|
(
|
||
|
// Panning is an execption. We don't want to apply
|
||
|
// overscroll when panning over the dataMax
|
||
|
!axis.chart.mouseIsDown ||
|
||
|
axis.isInternal) && (
|
||
|
// Scrollbar buttons are the other execption:
|
||
|
!axis.eventArgs ||
|
||
|
axis.eventArgs && axis.eventArgs.trigger !== 'navigator')) {
|
||
|
axis.max += axis.options.overscroll;
|
||
|
// Live data and buttons require translation for the min:
|
||
|
if (!axis.isInternal && defined(axis.userMin)) {
|
||
|
axis.min += axis.options.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
|
||
|
addEvent(AxisClass, 'afterSetScale', function () {
|
||
|
var axis = this;
|
||
|
if (axis.horiz && !axis.isDirty) {
|
||
|
axis.isDirty = axis.isOrdinal &&
|
||
|
axis.chart.navigator &&
|
||
|
!axis.chart.navigator.adaptToUpdatedData;
|
||
|
}
|
||
|
});
|
||
|
// Extending the Chart.pan method for ordinal axes
|
||
|
addEvent(ChartClass, 'pan', function (e) {
|
||
|
var chart = this, xAxis = chart.xAxis[0], overscroll = xAxis.options.overscroll, chartX = e.originalEvent.chartX, panning = chart.options.chart &&
|
||
|
chart.options.chart.panning, runBase = false;
|
||
|
if (panning &&
|
||
|
panning.type !== 'y' &&
|
||
|
xAxis.options.ordinal &&
|
||
|
xAxis.series.length) {
|
||
|
var mouseDownX = chart.mouseDownX, extremes = xAxis.getExtremes(), dataMax = extremes.dataMax, min = extremes.min, max = extremes.max, trimmedRange, hoverPoints = chart.hoverPoints, closestPointRange = (xAxis.closestPointRange ||
|
||
|
(xAxis.ordinal && xAxis.ordinal.overscrollPointsRange)), pointPixelWidth = (xAxis.translationSlope *
|
||
|
(xAxis.ordinal.slope || closestPointRange)),
|
||
|
// how many ordinal units did we move?
|
||
|
movedUnits = (mouseDownX - chartX) / pointPixelWidth,
|
||
|
// get index of all the chart's points
|
||
|
extendedAxis = { ordinal: { positions: xAxis.ordinal.getExtendedPositions() } }, ordinalPositions, searchAxisLeft, lin2val = xAxis.lin2val, val2lin = xAxis.val2lin, searchAxisRight;
|
||
|
// 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();
|
||
|
});
|
||
|
}
|
||
|
if (movedUnits < 0) {
|
||
|
searchAxisLeft = extendedAxis;
|
||
|
searchAxisRight = xAxis.ordinal.positions ? xAxis : extendedAxis;
|
||
|
}
|
||
|
else {
|
||
|
searchAxisLeft = xAxis.ordinal.positions ? xAxis : extendedAxis;
|
||
|
searchAxisRight = extendedAxis;
|
||
|
}
|
||
|
// 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 = searchAxisRight.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.fixedRange = max - min;
|
||
|
trimmedRange = xAxis.navigatorAxis.toFixedRange(null, null, lin2val.apply(searchAxisLeft, [
|
||
|
val2lin.apply(searchAxisLeft, [min, true]) + movedUnits,
|
||
|
true // translate from index
|
||
|
]), lin2val.apply(searchAxisRight, [
|
||
|
val2lin.apply(searchAxisRight, [max, true]) + movedUnits,
|
||
|
true // translate from index
|
||
|
]));
|
||
|
// Apply it if it is within the available data range
|
||
|
if (trimmedRange.min >= Math.min(extremes.dataMin, min) &&
|
||
|
trimmedRange.max <= Math.max(dataMax, 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();
|
||
|
}
|
||
|
});
|
||
|
addEvent(SeriesClass, 'updatedData', function () {
|
||
|
var xAxis = this.xAxis;
|
||
|
// Destroy the extended ordinal index on updated data
|
||
|
if (xAxis && xAxis.options.ordinal) {
|
||
|
delete xAxis.ordinal.index;
|
||
|
}
|
||
|
});
|
||
|
/* eslint-enable no-invalid-this */
|
||
|
};
|
||
|
return OrdinalAxis;
|
||
|
}());
|
||
|
OrdinalAxis.compose(Axis, Chart, Series); // @todo move to StockChart, remove from master
|
||
|
export default OrdinalAxis;
|