highcharts
Version:
JavaScript charting framework
1,195 lines • 62.6 kB
JavaScript
/* *
*
* (c) 2010-2025 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import Axis from '../../Core/Axis/Axis.js';
import ChartNavigatorComposition from './ChartNavigatorComposition.js';
import D from '../../Core/Defaults.js';
const { defaultOptions } = D;
import H from '../../Core/Globals.js';
const { isTouchDevice } = H;
import NavigatorAxisAdditions from '../../Core/Axis/NavigatorAxisComposition.js';
import NavigatorComposition from './NavigatorComposition.js';
import Scrollbar from '../Scrollbar/Scrollbar.js';
import SVGRenderer from '../../Core/Renderer/SVG/SVGRenderer.js';
const { prototype: { symbols } } = SVGRenderer;
import U from '../../Core/Utilities.js';
const { addEvent, clamp, correctFloat, defined, destroyObjectProperties, erase, extend, find, fireEvent, isArray, isNumber, merge, pick, removeEvent, splat } = U;
/* *
*
* Functions
*
* */
/**
* Finding the min or max of a set of variables where we don't know if they are
* defined, is a pattern that is repeated several places in Highcharts. Consider
* making this a global utility method.
* @private
*/
function numExt(extreme, ...args) {
const numbers = [].filter.call(args, isNumber);
if (numbers.length) {
return Math[extreme].apply(0, numbers);
}
}
/* *
*
* Class
*
* */
/**
* The Navigator class
*
* @private
* @class
* @name Highcharts.Navigator
*
* @param {Highcharts.Chart} chart
* Chart object
*/
class Navigator {
/* *
*
* Static Properties
*
* */
static compose(ChartClass, AxisClass, SeriesClass) {
ChartNavigatorComposition.compose(ChartClass, Navigator);
NavigatorComposition.compose(ChartClass, AxisClass, SeriesClass);
}
/* *
*
* Constructor
*
* */
constructor(chart) {
this.isDirty = false;
this.scrollbarHeight = 0;
this.init(chart);
}
/* *
*
* Functions
*
* */
/**
* Draw one of the handles on the side of the zoomed range in the navigator.
*
* @private
* @function Highcharts.Navigator#drawHandle
*
* @param {number} x
* The x center for the handle
*
* @param {number} index
* 0 for left and 1 for right
*
* @param {boolean|undefined} inverted
* Flag for chart.inverted
*
* @param {string} verb
* Use 'animate' or 'attr'
*/
drawHandle(x, index, inverted, verb) {
const navigator = this, height = navigator.navigatorOptions.handles.height;
// Place it
navigator.handles[index][verb](inverted ? {
translateX: Math.round(navigator.left + navigator.height / 2),
translateY: Math.round(navigator.top + parseInt(x, 10) + 0.5 - height)
} : {
translateX: Math.round(navigator.left + parseInt(x, 10)),
translateY: Math.round(navigator.top + navigator.height / 2 - height / 2 - 1)
});
}
/**
* Render outline around the zoomed range
*
* @private
* @function Highcharts.Navigator#drawOutline
*
* @param {number} zoomedMin
* in pixels position where zoomed range starts
*
* @param {number} zoomedMax
* in pixels position where zoomed range ends
*
* @param {boolean|undefined} inverted
* flag if chart is inverted
*
* @param {string} verb
* use 'animate' or 'attr'
*/
drawOutline(zoomedMin, zoomedMax, inverted, verb) {
const navigator = this, maskInside = navigator.navigatorOptions.maskInside, outlineWidth = navigator.outline.strokeWidth(), halfOutline = outlineWidth / 2, outlineCorrection = (outlineWidth % 2) / 2, // #5800
scrollButtonSize = navigator.scrollButtonSize, navigatorSize = navigator.size, navigatorTop = navigator.top, height = navigator.height, lineTop = navigatorTop - halfOutline, lineBtm = navigatorTop + height;
let left = navigator.left, verticalMin, path;
if (inverted) {
verticalMin = navigatorTop + zoomedMax + outlineCorrection;
zoomedMax = navigatorTop + zoomedMin + outlineCorrection;
path = [
[
'M',
left + height,
navigatorTop - scrollButtonSize - outlineCorrection
],
// Top right of zoomed range
['L', left + height, verticalMin],
['L', left, verticalMin], // Top left of z.r.
['M', left, zoomedMax], // Bottom left of z.r.
['L', left + height, zoomedMax], // Bottom right of z.r.
[
'L',
left + height,
navigatorTop + navigatorSize + scrollButtonSize
]
];
if (maskInside) {
path.push(
// Upper left of zoomed range
['M', left + height, verticalMin - halfOutline],
// Upper right of z.r.
[
'L',
left + height,
zoomedMax + halfOutline
]);
}
}
else {
left -= scrollButtonSize;
zoomedMin += left + scrollButtonSize - outlineCorrection;
zoomedMax += left + scrollButtonSize - outlineCorrection;
path = [
// Left
['M', left, lineTop],
// Upper left of zoomed range
['L', zoomedMin, lineTop],
// Lower left of z.r.
['L', zoomedMin, lineBtm],
// Lower right of z.r.
['M', zoomedMax, lineBtm],
// Upper right of z.r.
['L', zoomedMax, lineTop],
// Right
[
'L',
left + navigatorSize + scrollButtonSize * 2,
lineTop
]
];
if (maskInside) {
path.push(
// Upper left of zoomed range
['M', zoomedMin - halfOutline, lineTop],
// Upper right of z.r.
['L', zoomedMax + halfOutline, lineTop]);
}
}
navigator.outline[verb]({
d: path
});
}
/**
* Render outline around the zoomed range
*
* @private
* @function Highcharts.Navigator#drawMasks
*
* @param {number} zoomedMin
* in pixels position where zoomed range starts
*
* @param {number} zoomedMax
* in pixels position where zoomed range ends
*
* @param {boolean|undefined} inverted
* flag if chart is inverted
*
* @param {string} verb
* use 'animate' or 'attr'
*/
drawMasks(zoomedMin, zoomedMax, inverted, verb) {
const navigator = this, left = navigator.left, top = navigator.top, navigatorHeight = navigator.height;
let height, width, x, y;
// Determine rectangle position & size
// According to (non)inverted position:
if (inverted) {
x = [left, left, left];
y = [top, top + zoomedMin, top + zoomedMax];
width = [navigatorHeight, navigatorHeight, navigatorHeight];
height = [
zoomedMin,
zoomedMax - zoomedMin,
navigator.size - zoomedMax
];
}
else {
x = [left, left + zoomedMin, left + zoomedMax];
y = [top, top, top];
width = [
zoomedMin,
zoomedMax - zoomedMin,
navigator.size - zoomedMax
];
height = [navigatorHeight, navigatorHeight, navigatorHeight];
}
navigator.shades.forEach((shade, i) => {
shade[verb]({
x: x[i],
y: y[i],
width: width[i],
height: height[i]
});
});
}
/**
* Generate and update DOM elements for a navigator:
*
* - main navigator group
*
* - all shades
*
* - outline
*
* - handles
*
* @private
* @function Highcharts.Navigator#renderElements
*/
renderElements() {
const navigator = this, navigatorOptions = navigator.navigatorOptions, maskInside = navigatorOptions.maskInside, chart = navigator.chart, inverted = chart.inverted, renderer = chart.renderer, mouseCursor = {
cursor: inverted ? 'ns-resize' : 'ew-resize'
},
// Create the main navigator group
navigatorGroup = navigator.navigatorGroup ??
(navigator.navigatorGroup = renderer
.g('navigator')
.attr({
zIndex: 8,
visibility: 'hidden'
})
.add());
// Create masks, each mask will get events and fill:
[
!maskInside,
maskInside,
!maskInside
].forEach((hasMask, index) => {
const shade = navigator.shades[index] ??
(navigator.shades[index] = renderer.rect()
.addClass('highcharts-navigator-mask' +
(index === 1 ? '-inside' : '-outside'))
.add(navigatorGroup));
if (!chart.styledMode) {
shade.attr({
fill: hasMask ? navigatorOptions.maskFill : 'rgba(0,0,0,0)'
});
if (index === 1) {
shade.css(mouseCursor);
}
}
});
// Create the outline:
if (!navigator.outline) {
navigator.outline = renderer.path()
.addClass('highcharts-navigator-outline')
.add(navigatorGroup);
}
if (!chart.styledMode) {
navigator.outline.attr({
'stroke-width': navigatorOptions.outlineWidth,
stroke: navigatorOptions.outlineColor
});
}
// Create the handlers:
if (navigatorOptions.handles?.enabled) {
const handlesOptions = navigatorOptions.handles, { height, width } = handlesOptions;
[0, 1].forEach((index) => {
const symbolName = handlesOptions.symbols[index];
if (!navigator.handles[index] ||
navigator.handles[index].symbolUrl !== symbolName) {
// Generate symbol from scratch if we're dealing with an URL
navigator.handles[index]?.destroy();
navigator.handles[index] = renderer.symbol(symbolName, -width / 2 - 1, 0, width, height, handlesOptions);
// Z index is 6 for right handle, 7 for left. Can't be 10,
// because of the tooltip in inverted chart (#2908).
navigator.handles[index].attr({ zIndex: 7 - index })
.addClass('highcharts-navigator-handle ' +
'highcharts-navigator-handle-' +
['left', 'right'][index]).add(navigatorGroup);
navigator.addMouseEvents();
// If the navigator symbol changed, update its path and name
}
else if (!navigator.handles[index].isImg &&
navigator.handles[index].symbolName !== symbolName) {
const symbolFn = symbols[symbolName], path = symbolFn.call(symbols, -width / 2 - 1, 0, width, height);
navigator.handles[index].attr({
d: path
});
navigator.handles[index].symbolName = symbolName;
}
if (chart.inverted) {
navigator.handles[index].attr({
rotation: 90,
rotationOriginX: Math.floor(-width / 2),
rotationOriginY: (height + width) / 2
});
}
if (!chart.styledMode) {
navigator.handles[index]
.attr({
fill: handlesOptions.backgroundColor,
stroke: handlesOptions.borderColor,
'stroke-width': handlesOptions.lineWidth,
width: handlesOptions.width,
height: handlesOptions.height,
x: -width / 2 - 1,
y: 0
})
.css(mouseCursor);
}
});
}
}
/**
* Update navigator
*
* @private
* @function Highcharts.Navigator#update
*
* @param {Highcharts.NavigatorOptions} options
* Options to merge in when updating navigator
*/
update(options, redraw = false) {
const chart = this.chart, invertedUpdate = chart.options.chart.inverted !==
chart.scrollbar?.options.vertical;
merge(true, chart.options.navigator, options);
this.navigatorOptions = chart.options.navigator || {};
this.setOpposite();
// Revert to destroy/init for navigator/scrollbar enabled toggle
if (defined(options.enabled) || invertedUpdate) {
this.destroy();
this.navigatorEnabled = options.enabled || this.navigatorEnabled;
return this.init(chart);
}
if (this.navigatorEnabled) {
this.isDirty = true;
if (options.adaptToUpdatedData === false) {
this.baseSeries.forEach((series) => {
removeEvent(series, 'updatedData', this.updatedDataHandler);
}, this);
}
if (options.adaptToUpdatedData) {
this.baseSeries.forEach((series) => {
series.eventsToUnbind.push(addEvent(series, 'updatedData', this.updatedDataHandler));
}, this);
}
// Update navigator series
if (options.series || options.baseSeries) {
this.setBaseSeries(void 0, false);
}
// Update navigator axis
if (options.height || options.xAxis || options.yAxis) {
this.height = options.height ?? this.height;
const offsets = this.getXAxisOffsets();
this.xAxis.update({
...options.xAxis,
offsets,
[chart.inverted ? 'width' : 'height']: this.height,
[chart.inverted ? 'height' : 'width']: void 0
}, false);
this.yAxis.update({
...options.yAxis,
[chart.inverted ? 'width' : 'height']: this.height
}, false);
}
}
if (redraw) {
chart.redraw();
}
}
/**
* Render the navigator
*
* @private
* @function Highcharts.Navigator#render
* @param {number} min
* X axis value minimum
* @param {number} max
* X axis value maximum
* @param {number} [pxMin]
* Pixel value minimum
* @param {number} [pxMax]
* Pixel value maximum
*/
render(min, max, pxMin, pxMax) {
const navigator = this, chart = navigator.chart, xAxis = navigator.xAxis, pointRange = xAxis.pointRange || 0, scrollbarXAxis = xAxis.navigatorAxis.fake ? chart.xAxis[0] : xAxis, navigatorEnabled = navigator.navigatorEnabled, rendered = navigator.rendered, inverted = chart.inverted, minRange = chart.xAxis[0].minRange, maxRange = chart.xAxis[0].options.maxRange, scrollButtonSize = navigator.scrollButtonSize;
let navigatorWidth, scrollbarLeft, scrollbarTop, scrollbarHeight = navigator.scrollbarHeight, navigatorSize, verb;
// Don't redraw while moving the handles (#4703).
if (this.hasDragged && !defined(pxMin)) {
return;
}
if (this.isDirty) {
// Update DOM navigator elements
this.renderElements();
}
min = correctFloat(min - pointRange / 2);
max = correctFloat(max + pointRange / 2);
// Don't render the navigator until we have data (#486, #4202, #5172).
if (!isNumber(min) || !isNumber(max)) {
// However, if navigator was already rendered, we may need to resize
// it. For example hidden series, but visible navigator (#6022).
if (rendered) {
pxMin = 0;
pxMax = pick(xAxis.width, scrollbarXAxis.width);
}
else {
return;
}
}
navigator.left = pick(xAxis.left,
// In case of scrollbar only, without navigator
chart.plotLeft + scrollButtonSize +
(inverted ? chart.plotWidth : 0));
let zoomedMax = navigator.size = navigatorSize = pick(xAxis.len, (inverted ? chart.plotHeight : chart.plotWidth) -
2 * scrollButtonSize);
if (inverted) {
navigatorWidth = scrollbarHeight;
}
else {
navigatorWidth = navigatorSize + 2 * scrollButtonSize;
}
// Get the pixel position of the handles
pxMin = pick(pxMin, xAxis.toPixels(min, true));
pxMax = pick(pxMax, xAxis.toPixels(max, true));
// Verify (#1851, #2238)
if (!isNumber(pxMin) || Math.abs(pxMin) === Infinity) {
pxMin = 0;
pxMax = navigatorWidth;
}
// Are we below the minRange? (#2618, #6191)
const newMin = xAxis.toValue(pxMin, true), newMax = xAxis.toValue(pxMax, true), currentRange = Math.abs(correctFloat(newMax - newMin));
if (currentRange < minRange) {
if (this.grabbedLeft) {
pxMin = xAxis.toPixels(newMax - minRange - pointRange, true);
}
else if (this.grabbedRight) {
pxMax = xAxis.toPixels(newMin + minRange + pointRange, true);
}
}
else if (defined(maxRange) &&
correctFloat(currentRange - pointRange) > maxRange) {
if (this.grabbedLeft) {
pxMin = xAxis.toPixels(newMax - maxRange - pointRange, true);
}
else if (this.grabbedRight) {
pxMax = xAxis.toPixels(newMin + maxRange + pointRange, true);
}
}
// Handles are allowed to cross, but never exceed the plot area
navigator.zoomedMax = clamp(Math.max(pxMin, pxMax), 0, zoomedMax);
navigator.zoomedMin = clamp(navigator.fixedWidth ?
navigator.zoomedMax - navigator.fixedWidth :
Math.min(pxMin, pxMax), 0, zoomedMax);
navigator.range = navigator.zoomedMax - navigator.zoomedMin;
zoomedMax = Math.round(navigator.zoomedMax);
const zoomedMin = Math.round(navigator.zoomedMin);
if (navigatorEnabled) {
navigator.navigatorGroup.attr({
visibility: 'inherit'
});
// Place elements
verb = rendered && !navigator.hasDragged ? 'animate' : 'attr';
navigator.drawMasks(zoomedMin, zoomedMax, inverted, verb);
navigator.drawOutline(zoomedMin, zoomedMax, inverted, verb);
if (navigator.navigatorOptions.handles.enabled) {
navigator.drawHandle(zoomedMin, 0, inverted, verb);
navigator.drawHandle(zoomedMax, 1, inverted, verb);
}
}
if (navigator.scrollbar) {
if (inverted) {
scrollbarTop = navigator.top - scrollButtonSize;
scrollbarLeft = navigator.left - scrollbarHeight +
(navigatorEnabled || !scrollbarXAxis.opposite ? 0 :
// Multiple axes has offsets:
(scrollbarXAxis.titleOffset || 0) +
// Self margin from the axis.title
scrollbarXAxis.axisTitleMargin);
scrollbarHeight = navigatorSize + 2 * scrollButtonSize;
}
else {
scrollbarTop = navigator.top + (navigatorEnabled ?
navigator.height :
-scrollbarHeight);
scrollbarLeft = navigator.left - scrollButtonSize;
}
// Reposition scrollbar
navigator.scrollbar.position(scrollbarLeft, scrollbarTop, navigatorWidth, scrollbarHeight);
// Keep scale 0-1
navigator.scrollbar.setRange(
// Use real value, not rounded because range can be very small
// (#1716)
navigator.zoomedMin / (navigatorSize || 1), navigator.zoomedMax / (navigatorSize || 1));
}
navigator.rendered = true;
this.isDirty = false;
fireEvent(this, 'afterRender');
}
/**
* Set up the mouse and touch events for the navigator
*
* @private
* @function Highcharts.Navigator#addMouseEvents
*/
addMouseEvents() {
const navigator = this, chart = navigator.chart, container = chart.container;
let eventsToUnbind = [], mouseMoveHandler, mouseUpHandler;
/**
* Create mouse events' handlers.
* Make them as separate functions to enable wrapping them:
*/
navigator.mouseMoveHandler = mouseMoveHandler = function (e) {
navigator.onMouseMove(e);
};
navigator.mouseUpHandler = mouseUpHandler = function (e) {
navigator.onMouseUp(e);
};
// Add shades and handles mousedown events
eventsToUnbind = navigator.getPartsEvents('mousedown');
eventsToUnbind.push(
// Add mouse move and mouseup events. These are bind to doc/div,
// because Navigator.grabbedSomething flags are stored in mousedown
// events
addEvent(chart.renderTo, 'mousemove', mouseMoveHandler), addEvent(container.ownerDocument, 'mouseup', mouseUpHandler),
// Touch events
addEvent(chart.renderTo, 'touchmove', mouseMoveHandler), addEvent(container.ownerDocument, 'touchend', mouseUpHandler));
eventsToUnbind.concat(navigator.getPartsEvents('touchstart'));
navigator.eventsToUnbind = eventsToUnbind;
// Data events
if (navigator.series && navigator.series[0]) {
eventsToUnbind.push(addEvent(navigator.series[0].xAxis, 'foundExtremes', function () {
chart.navigator.modifyNavigatorAxisExtremes();
}));
}
}
/**
* Generate events for handles and masks
*
* @private
* @function Highcharts.Navigator#getPartsEvents
*
* @param {string} eventName
* Event name handler, 'mousedown' or 'touchstart'
*
* @return {Array<Function>}
* An array of functions to remove navigator functions from the
* events again.
*/
getPartsEvents(eventName) {
const navigator = this, events = [];
['shades', 'handles'].forEach(function (name) {
navigator[name].forEach(function (navigatorItem, index) {
events.push(addEvent(navigatorItem.element, eventName, function (e) {
navigator[name + 'Mousedown'](e, index);
}));
});
});
return events;
}
/**
* Mousedown on a shaded mask, either:
*
* - will be stored for future drag&drop
*
* - will directly shift to a new range
*
* @private
* @function Highcharts.Navigator#shadesMousedown
*
* @param {Highcharts.PointerEventObject} e
* Mouse event
*
* @param {number} index
* Index of a mask in Navigator.shades array
*/
shadesMousedown(e, index) {
e = this.chart.pointer?.normalize(e) || e;
const navigator = this, chart = navigator.chart, xAxis = navigator.xAxis, zoomedMin = navigator.zoomedMin, navigatorSize = navigator.size, range = navigator.range;
let navigatorPosition = navigator.left, chartX = e.chartX, fixedMax, fixedMin, ext, left;
// For inverted chart, swap some options:
if (chart.inverted) {
chartX = e.chartY;
navigatorPosition = navigator.top;
}
if (index === 1) {
// Store information for drag&drop
navigator.grabbedCenter = chartX;
navigator.fixedWidth = range;
navigator.dragOffset = chartX - zoomedMin;
}
else {
// Shift the range by clicking on shaded areas
left = chartX - navigatorPosition - range / 2;
if (index === 0) {
left = Math.max(0, left);
}
else if (index === 2 && left + range >= navigatorSize) {
left = navigatorSize - range;
if (navigator.reversedExtremes) {
// #7713
left -= range;
fixedMin = navigator.getUnionExtremes().dataMin;
}
else {
// #2293, #3543
fixedMax = navigator.getUnionExtremes().dataMax;
}
}
if (left !== zoomedMin) { // It has actually moved
navigator.fixedWidth = range; // #1370
ext = xAxis.navigatorAxis.toFixedRange(left, left + range, fixedMin, fixedMax);
if (defined(ext.min)) { // #7411
fireEvent(this, 'setRange', {
min: Math.min(ext.min, ext.max),
max: Math.max(ext.min, ext.max),
redraw: true,
eventArguments: {
trigger: 'navigator'
}
});
}
}
}
}
/**
* Mousedown on a handle mask.
* Will store necessary information for drag&drop.
*
* @private
* @function Highcharts.Navigator#handlesMousedown
* @param {Highcharts.PointerEventObject} e
* Mouse event
* @param {number} index
* Index of a handle in Navigator.handles array
*/
handlesMousedown(e, index) {
e = this.chart.pointer?.normalize(e) || e;
const navigator = this, chart = navigator.chart, baseXAxis = chart.xAxis[0],
// For reversed axes, min and max are changed,
// so the other extreme should be stored
reverse = navigator.reversedExtremes;
if (index === 0) {
// Grab the left handle
navigator.grabbedLeft = true;
navigator.otherHandlePos = navigator.zoomedMax;
navigator.fixedExtreme = reverse ? baseXAxis.min : baseXAxis.max;
}
else {
// Grab the right handle
navigator.grabbedRight = true;
navigator.otherHandlePos = navigator.zoomedMin;
navigator.fixedExtreme = reverse ? baseXAxis.max : baseXAxis.min;
}
chart.setFixedRange(void 0);
}
/**
* Mouse move event based on x/y mouse position.
*
* @private
* @function Highcharts.Navigator#onMouseMove
*
* @param {Highcharts.PointerEventObject} e
* Mouse event
*/
onMouseMove(e) {
const navigator = this, chart = navigator.chart, navigatorSize = navigator.navigatorSize, range = navigator.range, dragOffset = navigator.dragOffset, inverted = chart.inverted;
let left = navigator.left, chartX;
// In iOS, a mousemove event with e.pageX === 0 is fired when holding
// the finger down in the center of the scrollbar. This should be
// ignored.
if (!e.touches || e.touches[0].pageX !== 0) { // #4696
e = chart.pointer?.normalize(e) || e;
chartX = e.chartX;
// Swap some options for inverted chart
if (inverted) {
left = navigator.top;
chartX = e.chartY;
}
// Drag left handle or top handle
if (navigator.grabbedLeft) {
navigator.hasDragged = true;
navigator.render(0, 0, chartX - left, navigator.otherHandlePos);
// Drag right handle or bottom handle
}
else if (navigator.grabbedRight) {
navigator.hasDragged = true;
navigator.render(0, 0, navigator.otherHandlePos, chartX - left);
// Drag scrollbar or open area in navigator
}
else if (navigator.grabbedCenter) {
navigator.hasDragged = true;
if (chartX < dragOffset) { // Outside left
chartX = dragOffset;
// Outside right
}
else if (chartX >
navigatorSize + dragOffset - range) {
chartX = navigatorSize + dragOffset - range;
}
navigator.render(0, 0, chartX - dragOffset, chartX - dragOffset + range);
}
if (navigator.hasDragged &&
navigator.scrollbar &&
pick(navigator.scrollbar.options.liveRedraw,
// By default, don't run live redraw on touch
// devices or if the chart is in boost.
!isTouchDevice &&
!this.chart.boosted)) {
e.DOMType = e.type;
setTimeout(function () {
navigator.onMouseUp(e);
}, 0);
}
}
}
/**
* Mouse up event based on x/y mouse position.
*
* @private
* @function Highcharts.Navigator#onMouseUp
* @param {Highcharts.PointerEventObject} e
* Mouse event
*/
onMouseUp(e) {
const navigator = this, chart = navigator.chart, xAxis = navigator.xAxis, scrollbar = navigator.scrollbar, DOMEvent = e.DOMEvent || e, inverted = chart.inverted, verb = navigator.rendered && !navigator.hasDragged ?
'animate' : 'attr';
let zoomedMax, zoomedMin, unionExtremes, fixedMin, fixedMax, ext;
if (
// MouseUp is called for both, navigator and scrollbar (that order),
// which causes calling afterSetExtremes twice. Prevent first call
// by checking if scrollbar is going to set new extremes (#6334)
(navigator.hasDragged && (!scrollbar || !scrollbar.hasDragged)) ||
e.trigger === 'scrollbar') {
unionExtremes = navigator.getUnionExtremes();
// When dragging one handle, make sure the other one doesn't change
if (navigator.zoomedMin === navigator.otherHandlePos) {
fixedMin = navigator.fixedExtreme;
}
else if (navigator.zoomedMax === navigator.otherHandlePos) {
fixedMax = navigator.fixedExtreme;
}
// Snap to right edge (#4076)
if (navigator.zoomedMax === navigator.size) {
fixedMax = navigator.reversedExtremes ?
unionExtremes.dataMin :
unionExtremes.dataMax;
}
// Snap to left edge (#7576)
if (navigator.zoomedMin === 0) {
fixedMin = navigator.reversedExtremes ?
unionExtremes.dataMax :
unionExtremes.dataMin;
}
ext = xAxis.navigatorAxis.toFixedRange(navigator.zoomedMin, navigator.zoomedMax, fixedMin, fixedMax);
if (defined(ext.min)) {
fireEvent(this, 'setRange', {
min: Math.min(ext.min, ext.max),
max: Math.max(ext.min, ext.max),
redraw: true,
animation: navigator.hasDragged ? false : null,
eventArguments: {
trigger: 'navigator',
triggerOp: 'navigator-drag',
DOMEvent: DOMEvent // #1838
}
});
}
}
if (e.DOMType !== 'mousemove' &&
e.DOMType !== 'touchmove') {
navigator.grabbedLeft = navigator.grabbedRight =
navigator.grabbedCenter = navigator.fixedWidth =
navigator.fixedExtreme = navigator.otherHandlePos =
navigator.hasDragged = navigator.dragOffset = null;
}
// Update position of navigator shades, outline and handles (#12573)
if (navigator.navigatorEnabled &&
isNumber(navigator.zoomedMin) &&
isNumber(navigator.zoomedMax)) {
zoomedMin = Math.round(navigator.zoomedMin);
zoomedMax = Math.round(navigator.zoomedMax);
if (navigator.shades) {
navigator.drawMasks(zoomedMin, zoomedMax, inverted, verb);
}
if (navigator.outline) {
navigator.drawOutline(zoomedMin, zoomedMax, inverted, verb);
}
if (navigator.navigatorOptions.handles.enabled &&
Object.keys(navigator.handles).length ===
navigator.handles.length) {
navigator.drawHandle(zoomedMin, 0, inverted, verb);
navigator.drawHandle(zoomedMax, 1, inverted, verb);
}
}
}
/**
* Removes the event handlers attached previously with addEvents.
*
* @private
* @function Highcharts.Navigator#removeEvents
*/
removeEvents() {
if (this.eventsToUnbind) {
this.eventsToUnbind.forEach(function (unbind) {
unbind();
});
this.eventsToUnbind = void 0;
}
this.removeBaseSeriesEvents();
}
/**
* Remove data events.
*
* @private
* @function Highcharts.Navigator#removeBaseSeriesEvents
*/
removeBaseSeriesEvents() {
const baseSeries = this.baseSeries || [];
if (this.navigatorEnabled && baseSeries[0]) {
if (this.navigatorOptions.adaptToUpdatedData !== false) {
baseSeries.forEach(function (series) {
removeEvent(series, 'updatedData', this.updatedDataHandler);
}, this);
}
// We only listen for extremes-events on the first baseSeries
if (baseSeries[0].xAxis) {
removeEvent(baseSeries[0].xAxis, 'foundExtremes', this.modifyBaseAxisExtremes);
}
}
}
/**
* Calculate the navigator xAxis offsets
*
* @private
*/
getXAxisOffsets() {
return (this.chart.inverted ?
[this.scrollButtonSize, 0, -this.scrollButtonSize, 0] :
[0, -this.scrollButtonSize, 0, this.scrollButtonSize]);
}
/**
* Initialize the Navigator object
*
* @private
* @function Highcharts.Navigator#init
*/
init(chart) {
const chartOptions = chart.options, navigatorOptions = chartOptions.navigator || {}, navigatorEnabled = navigatorOptions.enabled, scrollbarOptions = chartOptions.scrollbar || {}, scrollbarEnabled = scrollbarOptions.enabled, height = navigatorEnabled && navigatorOptions.height || 0, scrollbarHeight = scrollbarEnabled && scrollbarOptions.height || 0, scrollButtonSize = scrollbarOptions.buttonsEnabled && scrollbarHeight || 0;
this.handles = [];
this.shades = [];
this.chart = chart;
this.setBaseSeries();
this.height = height;
this.scrollbarHeight = scrollbarHeight;
this.scrollButtonSize = scrollButtonSize;
this.scrollbarEnabled = scrollbarEnabled;
this.navigatorEnabled = navigatorEnabled;
this.navigatorOptions = navigatorOptions;
this.scrollbarOptions = scrollbarOptions;
this.setOpposite();
const navigator = this, baseSeries = navigator.baseSeries, xAxisIndex = chart.xAxis.length, yAxisIndex = chart.yAxis.length, baseXaxis = baseSeries && baseSeries[0] && baseSeries[0].xAxis ||
chart.xAxis[0] || { options: {} };
chart.isDirtyBox = true;
if (navigator.navigatorEnabled) {
const offsets = this.getXAxisOffsets();
// An x axis is required for scrollbar also
navigator.xAxis = new Axis(chart, merge({
// Inherit base xAxis' break, ordinal options and overscroll
breaks: baseXaxis.options.breaks,
ordinal: baseXaxis.options.ordinal,
overscroll: baseXaxis.options.overscroll
}, navigatorOptions.xAxis, {
type: 'datetime',
yAxis: navigatorOptions.yAxis?.id,
index: xAxisIndex,
isInternal: true,
offset: 0,
keepOrdinalPadding: true, // #2436
startOnTick: false,
endOnTick: false,
// Inherit base xAxis' padding when ordinal is false (#16915).
minPadding: baseXaxis.options.ordinal ? 0 :
baseXaxis.options.minPadding,
maxPadding: baseXaxis.options.ordinal ? 0 :
baseXaxis.options.maxPadding,
zoomEnabled: false
}, chart.inverted ? {
offsets,
width: height
} : {
offsets,
height
}), 'xAxis');
navigator.yAxis = new Axis(chart, merge(navigatorOptions.yAxis, {
alignTicks: false,
offset: 0,
index: yAxisIndex,
isInternal: true,
reversed: pick((navigatorOptions.yAxis &&
navigatorOptions.yAxis.reversed), (chart.yAxis[0] && chart.yAxis[0].reversed), false), // #14060
zoomEnabled: false
}, chart.inverted ? {
width: height
} : {
height: height
}), 'yAxis');
// If we have a base series, initialize the navigator series
if (baseSeries || navigatorOptions.series.data) {
navigator.updateNavigatorSeries(false);
// If not, set up an event to listen for added series
}
else if (chart.series.length === 0) {
navigator.unbindRedraw = addEvent(chart, 'beforeRedraw', function () {
// We've got one, now add it as base
if (chart.series.length > 0 && !navigator.series) {
navigator.setBaseSeries();
navigator.unbindRedraw(); // Reset
}
});
}
navigator.reversedExtremes = (chart.inverted && !navigator.xAxis.reversed) || (!chart.inverted && navigator.xAxis.reversed);
// Render items, so we can bind events to them:
navigator.renderElements();
// Add mouse events
navigator.addMouseEvents();
// In case of scrollbar only, fake an x axis to get translation
}
else {
navigator.xAxis = {
chart,
navigatorAxis: {
fake: true
},
translate: function (value, reverse) {
const axis = chart.xAxis[0], ext = axis.getExtremes(), scrollTrackWidth = axis.len - 2 * scrollButtonSize, min = numExt('min', axis.options.min, ext.dataMin), valueRange = numExt('max', axis.options.max, ext.dataMax) - min;
return reverse ?
// From pixel to value
(value * valueRange / scrollTrackWidth) + min :
// From value to pixel
scrollTrackWidth * (value - min) / valueRange;
},
toPixels: function (value) {
return this.translate(value);
},
toValue: function (value) {
return this.translate(value, true);
}
};
navigator.xAxis.navigatorAxis.axis = navigator.xAxis;
navigator.xAxis.navigatorAxis.toFixedRange = (NavigatorAxisAdditions.prototype.toFixedRange.bind(navigator.xAxis.navigatorAxis));
}
// Initialize the scrollbar
if (chart.options.scrollbar?.enabled) {
const options = merge(chart.options.scrollbar, { vertical: chart.inverted });
if (!isNumber(options.margin)) {
options.margin = chart.inverted ? -3 : 3;
}
chart.scrollbar = navigator.scrollbar = new Scrollbar(chart.renderer, options, chart);
addEvent(navigator.scrollbar, 'changed', function (e) {
const range = navigator.size, to = range * this.to, from = range * this.from;
navigator.hasDragged = navigator.scrollbar.hasDragged;
navigator.render(0, 0, from, to);
if (this.shouldUpdateExtremes(e.DOMType)) {
setTimeout(function () {
navigator.onMouseUp(e);
});
}
});
}
// Add data events
navigator.addBaseSeriesEvents();
// Add redraw events
navigator.addChartEvents();
}
/**
* Set the opposite property on navigator
*
* @private
*/
setOpposite() {
const navigatorOptions = this.navigatorOptions, navigatorEnabled = this.navigatorEnabled, chart = this.chart;
this.opposite = pick(navigatorOptions.opposite, Boolean(!navigatorEnabled && chart.inverted)); // #6262
}
/**
* Get the union data extremes of the chart - the outer data extremes of the
* base X axis and the navigator axis.
*
* @private
* @function Highcharts.Navigator#getUnionExtremes
*/
getUnionExtremes(returnFalseOnNoBaseSeries) {
const baseAxis = this.chart.xAxis[0], time = this.chart.time, navAxis = this.xAxis, navAxisOptions = navAxis.options, baseAxisOptions = baseAxis.options;
let ret;
if (!returnFalseOnNoBaseSeries || baseAxis.dataMin !== null) {
ret = {
dataMin: pick(// #4053
time.parse(navAxisOptions?.min), numExt('min', time.parse(baseAxisOptions.min), baseAxis.dataMin, navAxis.dataMin, navAxis.min)),
dataMax: pick(time.parse(navAxisOptions?.max), numExt('max', time.parse(baseAxisOptions.max), baseAxis.dataMax, navAxis.dataMax, navAxis.max))
};
}
return ret;
}
/**
* Set the base series and update the navigator series from this. With a bit
* of modification we should be able to make this an API method to be called
* from the outside
*
* @private
* @function Highcharts.Navigator#setBaseSeries
* @param {Highcharts.SeriesOptionsType} [baseSeriesOptions]
* Additional series options for a navigator
* @param {boolean} [redraw]
* Whether to redraw after update.
*/
setBaseSeries(baseSeriesOptions, redraw) {
const chart = this.chart, baseSeries = this.baseSeries = [];
baseSeriesOptions = (baseSeriesOptions ||
chart.options && chart.options.navigator.baseSeries ||
(chart.series.length ?
// Find the first non-navigator series (#8430)
find(chart.series, (s) => (!s.options.isInternal)).index :
0));
// Iterate through series and add the ones that should be shown in
// navigator.
(chart.series || []).forEach((series, i) => {
if (
// Don't include existing nav series
!series.options.isInternal &&
(series.options.showInNavigator ||
(i === baseSeriesOptions ||
series.options.id === baseSeriesOptions) &&
series.options.showInNavigator !== false)) {
baseSeries.push(series);
}
});
// When run after render, this.xAxis already exists
if (this.xAxis && !this.xAxis.navigatorAxis.fake) {
this.updateNavigatorSeries(true, redraw);
}
}
/**
* Update series in the navigator from baseSeries, adding new if does not
* exist.
*
* @private
* @function Highcharts.Navigator.updateNavigatorSeries
*/
updateNavigatorSeries(addEvents, redraw) {
const navigator = this, chart = navigator.chart, baseSeries = navigator.baseSeries, navSeriesMixin = {
enableMouseTracking: false,
index: null, // #6162
linkedTo: null, // #6734
group: 'nav', // For columns
padXAxis: false,
xAxis: this.navigatorOptions.xAxis?.id,
yAxis: this.navigatorOptions.yAxis?.id,
showInLegend: false,
stacking: void 0, // #4823
isInternal: true,
states: {
inactive: {
opacity: 1
}
}
},
// Remove navigator series that are no longer in the baseSeries
navigatorSeries = navigator.series =
(navigator.series || []).filter((navSeries) => {
const base = navSeries.baseSeries;
if (baseSeries.indexOf(base) < 0) { // Not in array
// If there is still a base series connected to this
// series, remove event handler and reference.
if (base) {
removeEvent(base, 'updatedData', navigator.updatedDataHandler);
delete base.navigatorSeries;
}
// Kill the nav series. It may already have been
// destroyed (#8715).
if (navSeries.chart) {
navSeries.destroy();
}
return false;
}
return true;
});
let baseOptions, mergedNavSeriesOptions, chartNavigatorSeriesOptions = navigator.navigatorOptions.series, baseNavigatorOptions;
// Go through each base series and merge the options to create new
// series
if (baseSeries && baseSeries.length) {
baseSeries.forEach((base) => {
const linkedNavSeries = base.navigatorSeries, userNavOptions = extend(
// Grab color and visibility from base as default
{
color: base.color,
visible: base.visible
}, !isArray(chartNavigatorSeriesOptions) ?
chartNavigatorSeriesOptions :
defaultOptions.navigator.series);
// Don't update if the series exists in nav and we have disabled
// adaptToUpdatedData.
if (linkedNavSeries &&
navigator.navigatorOptions.adaptToUpdatedData === false) {
return;
}
navSeriesMixin.name = 'Navigator ' + baseSeries.length;
baseOptions = base.options || {};
baseNavigatorOptions = baseOptions.navigatorOptions || {};
// The dataLabels options are not merged correctly
// if the settings are an array, #13847.
userNavOptions.dataLabels = splat(userNavOptions.dataLabels);
mergedNavSeriesOptions = merge(baseOptions, navSeriesMixin, userNavOptions, baseNavigatorOptions);
// Once nav series type is resolved, pick correct pointRange
mergedNavSeriesOptions.pointRange = pick(
// Stricte set pointRange in options
userNavOptions.pointRange, baseNavigatorOptions.pointRange,
// Fallback to default values, e.g. `null` for column
defaultOptions.plotOptions[mergedNavSeriesOptions.type || 'line'].pointRange);
// Merge data separately. Do a slice to avoid mutating the
// navigator options from base series (#4923).
const navigatorSeriesData = baseNavigatorOptions.data || userNavOptions.data;
navigator.hasNavigatorData =
navigator.hasNavigatorData || !!navigatorSeriesData;
mergedNavSeriesOptions.data = (navigatorSeriesData ||
baseOptions.data?.slice(0));
// Update or add the series
if (linkedNavSeries && linkedNavSeries.options) {
linkedNavSeries.update(mergedNavSeriesOptions, redraw);
}
else {
base.navigatorSeries = chart.initSeries(mergedNavSeriesOptions);
// Set data on initial run with dataSorting enabled (#20318)
chart.setSortedData();
base.navigatorSeries.baseSeries = base; // Store ref
navigatorSeries.push(base.navigatorSeries);
}
});
}
// If user has defined data (and no base series) or explicitly defined
// navigator.series as an array, we create these series on top of any
// base series.
if (chartNavigatorSeriesOptions.data &&
!(baseSeries && baseSeries.length) ||
isArray(chartNavigatorSeriesOptions)) {
navigator.hasNavigatorData = false;
// Allow navigator.series to be an array
chartNavigatorSeriesOptions =
splat(chartNavigatorSeriesOptions);
chartNavigatorSeriesOptions.forEach((userSeriesOptions, i) => {
navSeriesMixin.name =
'Navig