UNPKG

highcharts

Version:
1,195 lines 62.6 kB
/* * * * (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