/* *
|
*
|
* (c) 2010-2019 Torstein Honsi
|
*
|
* License: www.highcharts.com/license
|
*
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
*
|
* */
|
'use strict';
|
|
var _Globals = require('./Globals.js');
|
|
var _Globals2 = _interopRequireDefault(_Globals);
|
|
require('./Axis.js');
|
|
var _Utilities = require('./Utilities.js');
|
|
var _Utilities2 = _interopRequireDefault(_Utilities);
|
|
require('./Chart.js');
|
|
require('./Series.js');
|
|
require('./Navigator.js');
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
var defined = _Utilities2.default.defined;
|
// Has a dependency on Navigator due to the use of Axis.toFixedRange
|
|
var addEvent = _Globals2.default.addEvent,
|
Axis = _Globals2.default.Axis,
|
Chart = _Globals2.default.Chart,
|
css = _Globals2.default.css,
|
extend = _Globals2.default.extend,
|
noop = _Globals2.default.noop,
|
pick = _Globals2.default.pick,
|
Series = _Globals2.default.Series,
|
timeUnits = _Globals2.default.timeUnits;
|
/* eslint-disable no-invalid-this, valid-jsdoc */
|
/* ************************************************************************** *
|
* Start ordinal axis logic *
|
* ************************************************************************** */
|
addEvent(Series, 'updatedData', function () {
|
var xAxis = this.xAxis;
|
// Destroy the extended ordinal index on updated data
|
if (xAxis && xAxis.options.ordinal) {
|
delete xAxis.ordinalIndex;
|
}
|
});
|
/**
|
* 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
|
*/
|
Axis.prototype.getTimeTicks = function (normalizedInterval, min, max, startOfWeek, positions, closestDistance, findHigherRanks) {
|
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 || 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 = undefined;
|
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;
|
};
|
// Extend the Axis prototype
|
extend(Axis.prototype, /** @lends Axis.prototype */{
|
/**
|
* Calculate the ordinal positions before tick positions are calculated.
|
*
|
* @private
|
* @function Highcharts.Axis#beforeSetTickPositions
|
* @return {void}
|
*/
|
beforeSetTickPositions: function beforeSetTickPositions() {
|
var axis = this,
|
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 = axis.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) {
|
axis.overscrollPointsRange = overscrollPointsRange;
|
ordinalPositions = ordinalPositions.concat(axis.getOverscrollPositions());
|
}
|
// Register
|
axis.ordinalPositions = 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
|
axis.ordinalSlope = slope = (max - min) / (maxIndex - minIndex);
|
axis.ordinalOffset = min - minIndex * slope;
|
} else {
|
axis.overscrollPointsRange = pick(axis.closestPointRange, axis.overscrollPointsRange);
|
axis.ordinalPositions = axis.ordinalSlope = axis.ordinalOffset = undefined;
|
}
|
}
|
axis.isOrdinal = isOrdinal && useOrdinal; // #3818, #4196, #4926
|
axis.groupIntervalFactor = null; // reset for next run
|
},
|
/**
|
* 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}
|
*/
|
val2lin: function val2lin(val, toIndex) {
|
var axis = this,
|
ordinalPositions = axis.ordinalPositions,
|
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 : axis.ordinalSlope * (ordinalIndex || 0) + axis.ordinalOffset;
|
}
|
return ret;
|
},
|
/**
|
* 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}
|
*/
|
lin2val: function lin2val(val, fromIndex) {
|
var axis = this,
|
ordinalPositions = axis.ordinalPositions,
|
ret;
|
// the visible range contains only equally spaced values
|
if (!ordinalPositions) {
|
ret = val;
|
} else {
|
var ordinalSlope = axis.ordinalSlope,
|
ordinalOffset = axis.ordinalOffset,
|
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 distance !== undefined && ordinalPositions[i] !== undefined ? ordinalPositions[i] + (distance ? distance * (ordinalPositions[i + 1] - ordinalPositions[i]) : 0) : val;
|
}
|
return ret;
|
},
|
/**
|
* 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
|
* @function Highcharts.Axis#getExtendedPositions
|
*
|
* @return {Array<number>}
|
*/
|
getExtendedPositions: function getExtendedPositions() {
|
var axis = this,
|
chart = axis.chart,
|
grouping = axis.series[0].currentDataGrouping,
|
ordinalIndex = axis.ordinalIndex,
|
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 = axis.ordinalIndex = {};
|
}
|
if (!ordinalIndex[key]) {
|
// Create a fake axis object where the extended ordinal positions
|
// are emulated
|
fakeAxis = {
|
series: [],
|
chart: chart,
|
getExtremes: function getExtremes() {
|
return {
|
min: extremes.dataMin,
|
max: extremes.dataMax + overscroll
|
};
|
},
|
options: {
|
ordinal: true
|
},
|
val2lin: Axis.prototype.val2lin,
|
ordinal2lin: Axis.prototype.ordinal2lin // #6276
|
};
|
// 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: noop
|
};
|
fakeSeries.xData = fakeSeries.xData.concat(axis.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.ordinalPositions;
|
}
|
return ordinalIndex[key];
|
},
|
/**
|
* 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
|
* @function Highcharts.Axis#getOverscrollPositions
|
*
|
* @returns {Array<number>}
|
* Generated ticks
|
*/
|
getOverscrollPositions: function getOverscrollPositions() {
|
var axis = this,
|
extraRange = axis.options.overscroll,
|
distance = axis.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;
|
},
|
/**
|
* 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
|
* @function Highcharts.Axis#getGroupIntervalFactor
|
*
|
* @param {number} xMin
|
*
|
* @param {number} xMax
|
*
|
* @param {Highcharts.Series} series
|
*
|
* @return {number}
|
*/
|
getGroupIntervalFactor: function getGroupIntervalFactor(xMin, xMax, series) {
|
var i,
|
processedXData = series.processedXData,
|
len = processedXData.length,
|
distances = [],
|
median,
|
groupIntervalFactor = this.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]);
|
this.groupIntervalFactor = groupIntervalFactor = len * median / (xMax - xMin);
|
}
|
// Return the factor needed for data grouping
|
return groupIntervalFactor;
|
},
|
/**
|
* Make the tick intervals closer because the ordinal gaps make the ticks
|
* spread out or cluster.
|
*
|
* @private
|
* @function Highcharts.Axis#postProcessTickInterval
|
*
|
* @param {number} tickInterval
|
*
|
* @return {number}
|
*/
|
postProcessTickInterval: function 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
|
var ordinalSlope = this.ordinalSlope,
|
ret;
|
if (ordinalSlope) {
|
if (!this.options.breaks) {
|
ret = tickInterval / (ordinalSlope / this.closestPointRange);
|
} else {
|
ret = this.closestPointRange || tickInterval; // #7275
|
}
|
} else {
|
ret = tickInterval;
|
}
|
return ret;
|
}
|
});
|
// Record this to prevent overwriting by broken-axis module (#5979)
|
Axis.prototype.ordinal2lin = Axis.prototype.val2lin;
|
// Extending the Chart.pan method for ordinal axes
|
addEvent(Chart, 'pan', function (e) {
|
var chart = this,
|
xAxis = chart.xAxis[0],
|
overscroll = xAxis.options.overscroll,
|
chartX = e.originalEvent.chartX,
|
runBase = false;
|
if (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.overscrollPointsRange,
|
pointPixelWidth = xAxis.translationSlope * (xAxis.ordinalSlope || closestPointRange),
|
|
// how many ordinal units did we move?
|
movedUnits = (mouseDownX - chartX) / pointPixelWidth,
|
|
// get index of all the chart's points
|
extendedAxis = { ordinalPositions: xAxis.getExtendedPositions() },
|
ordinalPositions,
|
searchAxisLeft,
|
lin2val = xAxis.lin2val,
|
val2lin = xAxis.val2lin,
|
searchAxisRight;
|
// we have an ordinal axis, but the data is equally spaced
|
if (!extendedAxis.ordinalPositions) {
|
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.ordinalPositions ? xAxis : extendedAxis;
|
} else {
|
searchAxisLeft = xAxis.ordinalPositions ? 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.ordinalPositions;
|
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.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) {
|
if (overscroll) {
|
xAxis.max = xAxis.dataMax + overscroll;
|
}
|
} else {
|
e.preventDefault();
|
}
|
});
|
addEvent(Axis, '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(Axis, 'afterSetScale', function () {
|
var axis = this;
|
if (axis.horiz && !axis.isDirty) {
|
axis.isDirty = axis.isOrdinal && axis.chart.navigator && !axis.chart.navigator.adaptToUpdatedData;
|
}
|
});
|