/** * @license Highcharts JS v11.4.1 (2024-04-04) * * Standalone navigator module * * (c) 2009-2024 Mateusz Bernacik * * License: www.highcharts.com/license */ (function (factory) { if (typeof module === 'object' && module.exports) { factory['default'] = factory; module.exports = factory; } else if (typeof define === 'function' && define.amd) { define('highcharts/modules/navigator', ['highcharts'], function (Highcharts) { factory(Highcharts); factory.Highcharts = Highcharts; return factory; }); } else { factory(typeof Highcharts !== 'undefined' ? Highcharts : undefined); } }(function (Highcharts) { 'use strict'; var _modules = Highcharts ? Highcharts._modules : {}; function _registerModule(obj, path, args, fn) { if (!obj.hasOwnProperty(path)) { obj[path] = fn.apply(null, args); if (typeof CustomEvent === 'function') { window.dispatchEvent(new CustomEvent( 'HighchartsModuleLoaded', { detail: { path: path, module: obj[path] } } )); } } } _registerModule(_modules, 'Stock/Navigator/ChartNavigatorComposition.js', [_modules['Core/Globals.js'], _modules['Core/Utilities.js']], function (H, U) { /* * * * (c) 2010-2024 Torstein Honsi * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ const { isTouchDevice } = H; const { addEvent, merge, pick } = U; /* * * * Constants * * */ const composedMembers = []; /* * * * Variables * * */ let NavigatorConstructor; /* * * * Functions * * */ /** * @private */ function compose(ChartClass, NavigatorClass) { if (U.pushUnique(composedMembers, ChartClass)) { const chartProto = ChartClass.prototype; NavigatorConstructor = NavigatorClass; chartProto.callbacks.push(onChartCallback); addEvent(ChartClass, 'afterAddSeries', onChartAfterAddSeries); addEvent(ChartClass, 'afterSetChartSize', onChartAfterSetChartSize); addEvent(ChartClass, 'afterUpdate', onChartAfterUpdate); addEvent(ChartClass, 'beforeRender', onChartBeforeRender); addEvent(ChartClass, 'beforeShowResetZoom', onChartBeforeShowResetZoom); addEvent(ChartClass, 'update', onChartUpdate); } } /** * Handle adding new series. * @private */ function onChartAfterAddSeries() { if (this.navigator) { // Recompute which series should be shown in navigator, and add them this.navigator.setBaseSeries(null, false); } } /** * For stock charts, extend the Chart.setChartSize method so that we can set the * final top position of the navigator once the height of the chart, including * the legend, is determined. #367. We can't use Chart.getMargins, because * labels offsets are not calculated yet. * @private */ function onChartAfterSetChartSize() { const legend = this.legend, navigator = this.navigator; let legendOptions, xAxis, yAxis; if (navigator) { legendOptions = legend && legend.options; xAxis = navigator.xAxis; yAxis = navigator.yAxis; const { scrollbarHeight, scrollButtonSize } = navigator; // Compute the top position if (this.inverted) { navigator.left = navigator.opposite ? this.chartWidth - scrollbarHeight - navigator.height : this.spacing[3] + scrollbarHeight; navigator.top = this.plotTop + scrollButtonSize; } else { navigator.left = pick(xAxis.left, this.plotLeft + scrollButtonSize); navigator.top = navigator.navigatorOptions.top || this.chartHeight - navigator.height - scrollbarHeight - (this.scrollbar?.options.margin || 0) - this.spacing[2] - (this.rangeSelector && this.extraBottomMargin ? this.rangeSelector.getHeight() : 0) - ((legendOptions && legendOptions.verticalAlign === 'bottom' && legendOptions.layout !== 'proximate' && // #13392 legendOptions.enabled && !legendOptions.floating) ? legend.legendHeight + pick(legendOptions.margin, 10) : 0) - (this.titleOffset ? this.titleOffset[2] : 0); } if (xAxis && yAxis) { // False if navigator is disabled (#904) if (this.inverted) { xAxis.options.left = yAxis.options.left = navigator.left; } else { xAxis.options.top = yAxis.options.top = navigator.top; } xAxis.setAxisSize(); yAxis.setAxisSize(); } } } /** * Initialize navigator, if no scrolling exists yet. * @private */ function onChartAfterUpdate(event) { if (!this.navigator && !this.scroller && (this.options.navigator.enabled || this.options.scrollbar.enabled)) { this.scroller = this.navigator = new NavigatorConstructor(this); if (pick(event.redraw, true)) { this.redraw(event.animation); // #7067 } } } /** * Initialize navigator for stock charts * @private */ function onChartBeforeRender() { const options = this.options; if (options.navigator.enabled || options.scrollbar.enabled) { this.scroller = this.navigator = new NavigatorConstructor(this); } } /** * For Stock charts. For x only zooming, do not to create the zoom button * because X axis zooming is already allowed by the Navigator and Range * selector. (#9285) * @private */ function onChartBeforeShowResetZoom() { const chartOptions = this.options, navigator = chartOptions.navigator, rangeSelector = chartOptions.rangeSelector; if (((navigator && navigator.enabled) || (rangeSelector && rangeSelector.enabled)) && ((!isTouchDevice && this.zooming.type === 'x') || (isTouchDevice && this.zooming.pinchType === 'x'))) { return false; } } /** * @private */ function onChartCallback(chart) { const navigator = chart.navigator; // Initialize the navigator if (navigator && chart.xAxis[0]) { const extremes = chart.xAxis[0].getExtremes(); navigator.render(extremes.min, extremes.max); } } /** * Merge options, if no scrolling exists yet * @private */ function onChartUpdate(e) { const navigatorOptions = (e.options.navigator || {}), scrollbarOptions = (e.options.scrollbar || {}); if (!this.navigator && !this.scroller && (navigatorOptions.enabled || scrollbarOptions.enabled)) { merge(true, this.options.navigator, navigatorOptions); merge(true, this.options.scrollbar, scrollbarOptions); delete e.options.navigator; delete e.options.scrollbar; } } /* * * * Default Export * * */ const ChartNavigatorComposition = { compose }; return ChartNavigatorComposition; }); _registerModule(_modules, 'Core/Axis/NavigatorAxisComposition.js', [_modules['Core/Globals.js'], _modules['Core/Utilities.js']], function (H, U) { /* * * * (c) 2010-2024 Torstein Honsi * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ const { isTouchDevice } = H; const { addEvent, correctFloat, defined, isNumber, pick } = U; /* * * * Functions * * */ /** * @private */ function onAxisInit() { const axis = this; if (!axis.navigatorAxis) { axis.navigatorAxis = new NavigatorAxisAdditions(axis); } } /** * For Stock charts, override selection zooming with some special features * because X axis zooming is already allowed by the Navigator and Range * selector. * @private */ function onAxisSetExtremes(e) { const axis = this, chart = axis.chart, chartOptions = chart.options, navigator = chartOptions.navigator, navigatorAxis = axis.navigatorAxis, pinchType = chart.zooming.pinchType, rangeSelector = chartOptions.rangeSelector, zoomType = chart.zooming.type; let zoomed; if (axis.isXAxis && (navigator?.enabled || rangeSelector?.enabled)) { // For y only zooming, ignore the X axis completely if (zoomType === 'y' && e.trigger === 'zoom') { zoomed = false; // For xy zooming, record the state of the zoom before zoom selection, // then when the reset button is pressed, revert to this state. This // should apply only if the chart is initialized with a range (#6612), // otherwise zoom all the way out. } else if (((e.trigger === 'zoom' && zoomType === 'xy') || (isTouchDevice && pinchType === 'xy')) && axis.options.range) { const previousZoom = navigatorAxis.previousZoom; // Minimum defined, zooming in if (defined(e.min)) { navigatorAxis.previousZoom = [axis.min, axis.max]; // Minimum undefined, resetting zoom } else if (previousZoom) { e.min = previousZoom[0]; e.max = previousZoom[1]; navigatorAxis.previousZoom = void 0; } } } if (typeof zoomed !== 'undefined') { e.preventDefault(); } } /* * * * Class * * */ /** * @private * @class */ class NavigatorAxisAdditions { /* * * * Static Functions * * */ /** * @private */ static compose(AxisClass) { if (!AxisClass.keepProps.includes('navigatorAxis')) { AxisClass.keepProps.push('navigatorAxis'); addEvent(AxisClass, 'init', onAxisInit); addEvent(AxisClass, 'setExtremes', onAxisSetExtremes); } } /* * * * Constructors * * */ constructor(axis) { this.axis = axis; } /* * * * Functions * * */ /** * @private */ destroy() { this.axis = void 0; } /** * Add logic to normalize the zoomed range in order to preserve the pressed * state of range selector buttons * * @private * @function Highcharts.Axis#toFixedRange */ toFixedRange(pxMin, pxMax, fixedMin, fixedMax) { const axis = this.axis, halfPointRange = (axis.pointRange || 0) / 2; let newMin = pick(fixedMin, axis.translate(pxMin, true, !axis.horiz)), newMax = pick(fixedMax, axis.translate(pxMax, true, !axis.horiz)); // Add/remove half point range to/from the extremes (#1172) if (!defined(fixedMin)) { newMin = correctFloat(newMin + halfPointRange); } if (!defined(fixedMax)) { newMax = correctFloat(newMax - halfPointRange); } if (!isNumber(newMin) || !isNumber(newMax)) { // #1195, #7411 newMin = newMax = void 0; } return { min: newMin, max: newMax }; } } /* * * * Default Export * * */ return NavigatorAxisAdditions; }); _registerModule(_modules, 'Stock/Navigator/NavigatorDefaults.js', [_modules['Core/Color/Color.js'], _modules['Core/Series/SeriesRegistry.js']], function (Color, SeriesRegistry) { /* * * * (c) 2010-2024 Torstein Honsi * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ const { parse: color } = Color; const { seriesTypes } = SeriesRegistry; /* * * * Constants * * */ /** * The navigator is a small series below the main series, displaying * a view of the entire data set. It provides tools to zoom in and * out on parts of the data as well as panning across the dataset. * * @product highstock gantt * @optionparent navigator */ const NavigatorDefaults = { /** * Whether the navigator and scrollbar should adapt to updated data * in the base X axis. When loading data async, as in the demo below, * this should be `false`. Otherwise new data will trigger navigator * redraw, which will cause unwanted looping. In the demo below, the * data in the navigator is set only once. On navigating, only the main * chart content is updated. * * @sample {highstock} stock/demo/lazy-loading/ * Set to false with async data loading * * @type {boolean} * @default true * @apioption navigator.adaptToUpdatedData */ /** * An integer identifying the index to use for the base series, or a * string representing the id of the series. * * **Note**: As of Highcharts 5.0, this is now a deprecated option. * Prefer [series.showInNavigator](#plotOptions.series.showInNavigator). * * @see [series.showInNavigator](#plotOptions.series.showInNavigator) * * @deprecated * @type {number|string} * @default 0 * @apioption navigator.baseSeries */ /** * Enable or disable the navigator. * * @sample {highstock} stock/navigator/enabled/ * Disable the navigator * * @type {boolean} * @default true * @apioption navigator.enabled */ /** * When the chart is inverted, whether to draw the navigator on the * opposite side. * * @type {boolean} * @default false * @since 5.0.8 * @apioption navigator.opposite */ /** * The height of the navigator. * * @sample {highstock} stock/navigator/height/ * A higher navigator */ height: 40, /** * The distance from the nearest element, the X axis or X axis labels. * * @sample {highstock} stock/navigator/margin/ * A margin of 2 draws the navigator closer to the X axis labels */ margin: 25, /** * Whether the mask should be inside the range marking the zoomed * range, or outside. In Highcharts Stock 1.x it was always `false`. * * @sample {highstock} stock/demo/maskinside-false/ * False, mask outside * * @since 2.0 */ maskInside: true, /** * Options for the handles for dragging the zoomed area. * * @sample {highstock} stock/navigator/handles/ * Colored handles */ handles: { /** * Width for handles. * * @sample {highstock} stock/navigator/styled-handles/ * Styled handles * * @since 6.0.0 */ width: 7, /** * Height for handles. * * @sample {highstock} stock/navigator/styled-handles/ * Styled handles * * @since 6.0.0 */ height: 15, /** * Array to define shapes of handles. 0-index for left, 1-index for * right. * * Additionally, the URL to a graphic can be given on this form: * `url(graphic.png)`. Note that for the image to be applied to * exported charts, its URL needs to be accessible by the export * server. * * Custom callbacks for symbol path generation can also be added to * `Highcharts.SVGRenderer.prototype.symbols`. The callback is then * used by its method name, as shown in the demo. * * @sample {highstock} stock/navigator/styled-handles/ * Styled handles * * @type {Array} * @default ["navigator-handle", "navigator-handle"] * @since 6.0.0 */ symbols: ['navigator-handle', 'navigator-handle'], /** * Allows to enable/disable handles. * * @since 6.0.0 */ enabled: true, /** * The width for the handle border and the stripes inside. * * @sample {highstock} stock/navigator/styled-handles/ * Styled handles * * @since 6.0.0 * @apioption navigator.handles.lineWidth */ lineWidth: 1, /** * The fill for the handle. * * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject} */ backgroundColor: "#f2f2f2" /* Palette.neutralColor5 */, /** * The stroke for the handle border and the stripes inside. * * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject} */ borderColor: "#999999" /* Palette.neutralColor40 */ }, /** * The color of the mask covering the areas of the navigator series * that are currently not visible in the main series. The default * color is bluish with an opacity of 0.3 to see the series below. * * @see In styled mode, the mask is styled with the * `.highcharts-navigator-mask` and * `.highcharts-navigator-mask-inside` classes. * * @sample {highstock} stock/navigator/maskfill/ * Blue, semi transparent mask * * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject} * @default rgba(102,133,194,0.3) */ maskFill: color("#667aff" /* Palette.highlightColor60 */).setOpacity(0.3).get(), /** * The color of the line marking the currently zoomed area in the * navigator. * * @sample {highstock} stock/navigator/outline/ * 2px blue outline * * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject} * @default #cccccc */ outlineColor: "#999999" /* Palette.neutralColor40 */, /** * The width of the line marking the currently zoomed area in the * navigator. * * @see In styled mode, the outline stroke width is set with the * `.highcharts-navigator-outline` class. * * @sample {highstock} stock/navigator/outline/ * 2px blue outline * * @type {number} */ outlineWidth: 1, /** * Options for the navigator series. Available options are the same * as any series, documented at [plotOptions](#plotOptions.series) * and [series](#series). * * Unless data is explicitly defined on navigator.series, the data * is borrowed from the first series in the chart. * * Default series options for the navigator series are: * ```js * series: { * type: 'areaspline', * fillOpacity: 0.05, * dataGrouping: { * smoothed: true * }, * lineWidth: 1, * marker: { * enabled: false * } * } * ``` * * @see In styled mode, the navigator series is styled with the * `.highcharts-navigator-series` class. * * @sample {highstock} stock/navigator/series-data/ * Using a separate data set for the navigator * @sample {highstock} stock/navigator/series/ * A green navigator series * * @type {*|Array<*>|Highcharts.SeriesOptionsType|Array} */ series: { /** * The type of the navigator series. * * Heads up: * In column-type navigator, zooming is limited to at least one * point with its `pointRange`. * * @sample {highstock} stock/navigator/column/ * Column type navigator * * @type {string} * @default {highstock} `areaspline` if defined, otherwise `line` * @default {gantt} gantt */ type: (typeof seriesTypes.areaspline === 'undefined' ? 'line' : 'areaspline'), /** * The fill opacity of the navigator series. */ fillOpacity: 0.05, /** * The pixel line width of the navigator series. */ lineWidth: 1, /** * @ignore-option */ compare: null, /** * @ignore-option */ sonification: { enabled: false }, /** * Unless data is explicitly defined, the data is borrowed from the * first series in the chart. * * @type {Array|object|null>} * @product highstock * @apioption navigator.series.data */ /** * Data grouping options for the navigator series. * * @extends plotOptions.series.dataGrouping */ dataGrouping: { approximation: 'average', enabled: true, groupPixelWidth: 2, // Replace smoothed property by anchors, #12455. firstAnchor: 'firstPoint', anchor: 'middle', lastAnchor: 'lastPoint', // Day and week differs from plotOptions.series.dataGrouping units: [ ['millisecond', [1, 2, 5, 10, 20, 25, 50, 100, 200, 500]], ['second', [1, 2, 5, 10, 15, 30]], ['minute', [1, 2, 5, 10, 15, 30]], ['hour', [1, 2, 3, 4, 6, 8, 12]], ['day', [1, 2, 3, 4]], ['week', [1, 2, 3]], ['month', [1, 3, 6]], ['year', null] ] }, /** * Data label options for the navigator series. Data labels are * disabled by default on the navigator series. * * @extends plotOptions.series.dataLabels */ dataLabels: { enabled: false, zIndex: 2 // #1839 }, id: 'highcharts-navigator-series', className: 'highcharts-navigator-series', /** * Sets the fill color of the navigator series. * * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject} * @apioption navigator.series.color */ /** * Line color for the navigator series. Allows setting the color * while disallowing the default candlestick setting. * * @type {Highcharts.ColorString|null} */ lineColor: null, marker: { enabled: false }, /** * Since Highcharts Stock v8, default value is the same as default * `pointRange` defined for a specific type (e.g. `null` for * column type). * * In Highcharts Stock version < 8, defaults to 0. * * @extends plotOptions.series.pointRange * @type {number|null} * @apioption navigator.series.pointRange */ /** * The threshold option. Setting it to 0 will make the default * navigator area series draw its area from the 0 value and up. * * @type {number|null} */ threshold: null }, /** * Enable or disable navigator sticking to right, while adding new * points. If `undefined`, the navigator sticks to the axis maximum only * if it was already at the maximum prior to adding points. * * @type {boolean} * @default undefined * @since 10.2.1 * @sample {highstock} stock/navigator/sticktomax-false/ * stickToMax set to false * @apioption navigator.stickToMax */ /** * Options for the navigator X axis. Default series options for the * navigator xAxis are: * ```js * xAxis: { * tickWidth: 0, * lineWidth: 0, * gridLineWidth: 1, * tickPixelInterval: 200, * labels: { * align: 'left', * style: { * color: '#888' * }, * x: 3, * y: -4 * } * } * ``` * * @extends xAxis * @excluding linkedTo, maxZoom, minRange, opposite, range, scrollbar, * showEmpty, maxRange */ xAxis: { /** * Additional range on the right side of the xAxis. Works similar to * `xAxis.maxPadding`, but the value is set in terms of axis values, * percentage or pixels. * * If it's a number, it is interpreted as axis values, which in a * datetime axis equals milliseconds. * * If it's a percentage string, is interpreted as percentages of the * axis length. An overscroll of 50% will make a 100px axis 50px longer. * * If it's a pixel string, it is interpreted as a fixed pixel value, but * limited to 90% of the axis length. * * If it's undefined, the value is inherited from `xAxis.overscroll`. * * Can be set for both, main xAxis and navigator's xAxis. * * @type {number | string | undefined} * @since 6.0.0 * @apioption navigator.xAxis.overscroll */ className: 'highcharts-navigator-xaxis', tickLength: 0, lineWidth: 0, gridLineColor: "#e6e6e6" /* Palette.neutralColor10 */, gridLineWidth: 1, tickPixelInterval: 200, labels: { align: 'left', /** * @type {Highcharts.CSSObject} */ style: { /** @ignore */ color: "#000000" /* Palette.neutralColor100 */, /** @ignore */ fontSize: '0.7em', /** @ignore */ opacity: 0.6, /** @ignore */ textOutline: '2px contrast' }, x: 3, y: -4 }, crosshair: false }, /** * Options for the navigator Y axis. Default series options for the * navigator yAxis are: * ```js * yAxis: { * gridLineWidth: 0, * startOnTick: false, * endOnTick: false, * minPadding: 0.1, * maxPadding: 0.1, * labels: { * enabled: false * }, * title: { * text: null * }, * tickWidth: 0 * } * ``` * * @extends yAxis * @excluding height, linkedTo, maxZoom, minRange, ordinal, range, * showEmpty, scrollbar, top, units, maxRange, minLength, * maxLength, resize */ yAxis: { className: 'highcharts-navigator-yaxis', gridLineWidth: 0, startOnTick: false, endOnTick: false, minPadding: 0.1, maxPadding: 0.1, labels: { enabled: false }, crosshair: false, title: { text: null }, tickLength: 0, tickWidth: 0 } }; /* * * * Default Export * * */ /* * * * API Options * * */ /** * Maximum range which can be set using the navigator's handles. * Opposite of [xAxis.minRange](#xAxis.minRange). * * @sample {highstock} stock/navigator/maxrange/ * Defined max and min range * * @type {number} * @since 6.0.0 * @product highstock gantt * @apioption xAxis.maxRange */ (''); // Keeps doclets above in JS file return NavigatorDefaults; }); _registerModule(_modules, 'Stock/Navigator/NavigatorSymbols.js', [], function () { /* * * * (c) 2010-2024 Torstein Honsi * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ /* * * * Constants * * */ /** * Draw one of the handles on the side of the zoomed range in the navigator. * @private */ function navigatorHandle(_x, _y, width, height, options = {}) { const halfWidth = options.width ? options.width / 2 : width, markerPosition = Math.round(halfWidth / 3) + 0.5; height = options.height || height; return [ ['M', -halfWidth - 1, 0.5], ['L', halfWidth, 0.5], ['L', halfWidth, height + 0.5], ['L', -halfWidth - 1, height + 0.5], ['L', -halfWidth - 1, 0.5], ['M', -markerPosition, 4], ['L', -markerPosition, height - 3], ['M', markerPosition - 1, 4], ['L', markerPosition - 1, height - 3] ]; } /* * * * Default Export * * */ const NavigatorSymbols = { 'navigator-handle': navigatorHandle }; return NavigatorSymbols; }); _registerModule(_modules, 'Stock/Utilities/StockUtilities.js', [_modules['Core/Utilities.js']], function (U) { /* * * * (c) 2010-2024 Torstein Honsi * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ const { defined } = U; /* * * * Functions * * */ /** * Sets the chart.fixedRange to the specified value. If the value is larger * than actual range, sets it to the maximum possible range. (#20327) * * @private * @function Highcharts.StockChart#setFixedRange * @param {number|undefined} range * Range to set in axis units. */ function setFixedRange(range) { const xAxis = this.xAxis[0]; if (defined(xAxis.dataMax) && defined(xAxis.dataMin) && range) { this.fixedRange = Math.min(range, xAxis.dataMax - xAxis.dataMin); } else { this.fixedRange = range; } } const StockUtilities = { setFixedRange }; return StockUtilities; }); _registerModule(_modules, 'Stock/Navigator/NavigatorComposition.js', [_modules['Core/Defaults.js'], _modules['Core/Globals.js'], _modules['Core/Axis/NavigatorAxisComposition.js'], _modules['Stock/Navigator/NavigatorDefaults.js'], _modules['Stock/Navigator/NavigatorSymbols.js'], _modules['Core/Renderer/RendererRegistry.js'], _modules['Stock/Utilities/StockUtilities.js'], _modules['Core/Utilities.js']], function (D, H, NavigatorAxisAdditions, NavigatorDefaults, NavigatorSymbols, RendererRegistry, StockUtilities, U) { /* * * * (c) 2010-2024 Torstein Honsi * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ const { setOptions } = D; const { composed } = H; const { getRendererType } = RendererRegistry; const { setFixedRange } = StockUtilities; const { addEvent, extend, pushUnique } = U; /* * * * Variables * * */ /* * * * Functions * * */ /** * @private */ function compose(ChartClass, AxisClass, SeriesClass) { NavigatorAxisAdditions.compose(AxisClass); if (pushUnique(composed, 'Navigator')) { ChartClass.prototype.setFixedRange = setFixedRange; extend(getRendererType().prototype.symbols, NavigatorSymbols); addEvent(SeriesClass, 'afterUpdate', onSeriesAfterUpdate); setOptions({ navigator: NavigatorDefaults }); } } /** * Handle updating series * @private */ function onSeriesAfterUpdate() { if (this.chart.navigator && !this.options.isInternal) { this.chart.navigator.setBaseSeries(null, false); } } /* * * * Default Export * * */ const NavigatorComposition = { compose }; return NavigatorComposition; }); _registerModule(_modules, 'Core/Axis/ScrollbarAxis.js', [_modules['Core/Globals.js'], _modules['Core/Utilities.js']], function (H, U) { /* * * * (c) 2010-2024 Torstein Honsi * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ const { composed } = H; const { addEvent, defined, pick, pushUnique } = U; /* * * * Composition * * */ var ScrollbarAxis; (function (ScrollbarAxis) { /* * * * Variables * * */ let Scrollbar; /* * * * Functions * * */ /** * Attaches to axis events to create scrollbars if enabled. * * @private * * @param {Highcharts.Axis} AxisClass * Axis class to extend. * * @param {Highcharts.Scrollbar} ScrollbarClass * Scrollbar class to use. */ function compose(AxisClass, ScrollbarClass) { if (pushUnique(composed, 'Axis.Scrollbar')) { Scrollbar = ScrollbarClass; addEvent(AxisClass, 'afterGetOffset', onAxisAfterGetOffset); addEvent(AxisClass, 'afterInit', onAxisAfterInit); addEvent(AxisClass, 'afterRender', onAxisAfterRender); } } ScrollbarAxis.compose = compose; /** @private */ function getExtremes(axis) { const axisMin = pick(axis.options && axis.options.min, axis.min); const axisMax = pick(axis.options && axis.options.max, axis.max); return { axisMin, axisMax, scrollMin: defined(axis.dataMin) ? Math.min(axisMin, axis.min, axis.dataMin, pick(axis.threshold, Infinity)) : axisMin, scrollMax: defined(axis.dataMax) ? Math.max(axisMax, axis.max, axis.dataMax, pick(axis.threshold, -Infinity)) : axisMax }; } /** * Make space for a scrollbar. * @private */ function onAxisAfterGetOffset() { const axis = this, scrollbar = axis.scrollbar, opposite = scrollbar && !scrollbar.options.opposite, index = axis.horiz ? 2 : opposite ? 3 : 1; if (scrollbar) { // Reset scrollbars offsets axis.chart.scrollbarsOffsets = [0, 0]; axis.chart.axisOffset[index] += scrollbar.size + (scrollbar.options.margin || 0); } } /** * Wrap axis initialization and create scrollbar if enabled. * @private */ function onAxisAfterInit() { const axis = this; if (axis.options && axis.options.scrollbar && axis.options.scrollbar.enabled) { // Predefined options: axis.options.scrollbar.vertical = !axis.horiz; axis.options.startOnTick = axis.options.endOnTick = false; axis.scrollbar = new Scrollbar(axis.chart.renderer, axis.options.scrollbar, axis.chart); addEvent(axis.scrollbar, 'changed', function (e) { const { axisMin, axisMax, scrollMin: unitedMin, scrollMax: unitedMax } = getExtremes(axis), range = unitedMax - unitedMin; let to, from; // #12834, scroll when show/hide series, wrong extremes if (!defined(axisMin) || !defined(axisMax)) { return; } if ((axis.horiz && !axis.reversed) || (!axis.horiz && axis.reversed)) { to = unitedMin + range * this.to; from = unitedMin + range * this.from; } else { // Y-values in browser are reversed, but this also // applies for reversed horizontal axis: to = unitedMin + range * (1 - this.from); from = unitedMin + range * (1 - this.to); } if (this.shouldUpdateExtremes(e.DOMType)) { // #17977, set animation to undefined instead of true const animate = e.DOMType === 'mousemove' || e.DOMType === 'touchmove' ? false : void 0; axis.setExtremes(from, to, true, animate, e); } else { // When live redraw is disabled, don't change extremes // Only change the position of the scrollbar thumb this.setRange(this.from, this.to); } }); } } /** * Wrap rendering axis, and update scrollbar if one is created. * @private */ function onAxisAfterRender() { const axis = this, { scrollMin, scrollMax } = getExtremes(axis), scrollbar = axis.scrollbar, offset = (axis.axisTitleMargin + (axis.titleOffset || 0)), scrollbarsOffsets = axis.chart.scrollbarsOffsets, axisMargin = axis.options.margin || 0; let offsetsIndex, from, to; if (scrollbar && scrollbarsOffsets) { if (axis.horiz) { // Reserve space for labels/title if (!axis.opposite) { scrollbarsOffsets[1] += offset; } scrollbar.position(axis.left, (axis.top + axis.height + 2 + scrollbarsOffsets[1] - (axis.opposite ? axisMargin : 0)), axis.width, axis.height); // Next scrollbar should reserve space for margin (if set) if (!axis.opposite) { scrollbarsOffsets[1] += axisMargin; } offsetsIndex = 1; } else { // Reserve space for labels/title if (axis.opposite) { scrollbarsOffsets[0] += offset; } let xPosition; if (!scrollbar.options.opposite) { xPosition = axis.opposite ? 0 : axisMargin; } else { xPosition = axis.left + axis.width + 2 + scrollbarsOffsets[0] - (axis.opposite ? 0 : axisMargin); } scrollbar.position(xPosition, axis.top, axis.width, axis.height); // Next scrollbar should reserve space for margin (if set) if (axis.opposite) { scrollbarsOffsets[0] += axisMargin; } offsetsIndex = 0; } scrollbarsOffsets[offsetsIndex] += scrollbar.size + (scrollbar.options.margin || 0); if (isNaN(scrollMin) || isNaN(scrollMax) || !defined(axis.min) || !defined(axis.max) || axis.min === axis.max // #10733 ) { // Default action: when extremes are the same or there is // not extremes on the axis, but scrollbar exists, make it // full size scrollbar.setRange(0, 1); } else { from = ((axis.min - scrollMin) / (scrollMax - scrollMin)); to = ((axis.max - scrollMin) / (scrollMax - scrollMin)); if ((axis.horiz && !axis.reversed) || (!axis.horiz && axis.reversed)) { scrollbar.setRange(from, to); } else { // Inverse vertical axis scrollbar.setRange(1 - to, 1 - from); } } } } })(ScrollbarAxis || (ScrollbarAxis = {})); /* * * * Default Export * * */ return ScrollbarAxis; }); _registerModule(_modules, 'Stock/Scrollbar/ScrollbarDefaults.js', [], function () { /* * * * (c) 2010-2024 Torstein Honsi * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ /* * * * Constant * * */ /** * * The scrollbar is a means of panning over the X axis of a stock chart. * Scrollbars can also be applied to other types of axes. * * Another approach to scrollable charts is the [chart.scrollablePlotArea]( * https://api.highcharts.com/highcharts/chart.scrollablePlotArea) option that * is especially suitable for simpler cartesian charts on mobile. * * In styled mode, all the presentational options for the * scrollbar are replaced by the classes `.highcharts-scrollbar-thumb`, * `.highcharts-scrollbar-arrow`, `.highcharts-scrollbar-button`, * `.highcharts-scrollbar-rifles` and `.highcharts-scrollbar-track`. * * @sample stock/yaxis/inverted-bar-scrollbar/ * A scrollbar on a simple bar chart * * @product highstock gantt * @optionparent scrollbar * * @private */ const ScrollbarDefaults = { /** * The height of the scrollbar. If `buttonsEnabled` is true , the height * also applies to the width of the scroll arrows so that they are always * squares. * * @sample stock/scrollbar/style/ * Non-default height * * @type {number} */ height: 10, /** * The border rounding radius of the bar. * * @sample stock/scrollbar/style/ * Scrollbar styling */ barBorderRadius: 5, /** * The corner radius of the scrollbar buttons. * * @sample stock/scrollbar/style/ * Scrollbar styling */ buttonBorderRadius: 0, /** * Enable or disable the buttons at the end of the scrollbar. * * @since 11.0.0 */ buttonsEnabled: false, /** * Enable or disable the scrollbar. * * @sample stock/scrollbar/enabled/ * Disable the scrollbar, only use navigator * * @type {boolean} * @default true * @apioption scrollbar.enabled */ /** * Whether to redraw the main chart as the scrollbar or the navigator * zoomed window is moved. Defaults to `true` for modern browsers and * `false` for legacy IE browsers as well as mobile devices. * * @sample stock/scrollbar/liveredraw * Setting live redraw to false * * @type {boolean} * @since 1.3 */ liveRedraw: void 0, /** * The margin between the scrollbar and its axis when the scrollbar is * applied directly to an axis, or the navigator in case that is enabled. * Defaults to 10 for axis, 0 for navigator. * * @type {number|undefined} */ margin: void 0, /** * The minimum width of the scrollbar. * * @since 1.2.5 */ minWidth: 6, /** @ignore-option */ opposite: true, /** * Whether to show or hide the scrollbar when the scrolled content is * zoomed out to it full extent. * * @type {boolean} * @default true * @apioption scrollbar.showFull */ step: 0.2, /** * The z index of the scrollbar group. */ zIndex: 3, /** * The background color of the scrollbar itself. * * @sample stock/scrollbar/style/ * Scrollbar styling * * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject} */ barBackgroundColor: "#cccccc" /* Palette.neutralColor20 */, /** * The width of the bar's border. * * @sample stock/scrollbar/style/ * Scrollbar styling */ barBorderWidth: 0, /** * The color of the scrollbar's border. * * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject} */ barBorderColor: "#cccccc" /* Palette.neutralColor20 */, /** * The color of the small arrow inside the scrollbar buttons. * * @sample stock/scrollbar/style/ * Scrollbar styling * * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject} */ buttonArrowColor: "#333333" /* Palette.neutralColor80 */, /** * The color of scrollbar buttons. * * @sample stock/scrollbar/style/ * Scrollbar styling * * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject} */ buttonBackgroundColor: "#e6e6e6" /* Palette.neutralColor10 */, /** * The color of the border of the scrollbar buttons. * * @sample stock/scrollbar/style/ * Scrollbar styling * * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject} */ buttonBorderColor: "#cccccc" /* Palette.neutralColor20 */, /** * The border width of the scrollbar buttons. * * @sample stock/scrollbar/style/ * Scrollbar styling */ buttonBorderWidth: 1, /** * The color of the small rifles in the middle of the scrollbar. * * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject} */ rifleColor: 'none', /** * The color of the track background. * * @sample stock/scrollbar/style/ * Scrollbar styling * * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject} */ trackBackgroundColor: 'rgba(255, 255, 255, 0.001)', /** * The color of the border of the scrollbar track. * * @sample stock/scrollbar/style/ * Scrollbar styling * * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject} */ trackBorderColor: "#cccccc" /* Palette.neutralColor20 */, /** * The corner radius of the border of the scrollbar track. * * @sample stock/scrollbar/style/ * Scrollbar styling */ trackBorderRadius: 5, /** * The width of the border of the scrollbar track. * * @sample stock/scrollbar/style/ * Scrollbar styling */ trackBorderWidth: 1 }; /* * * * Default Export * * */ return ScrollbarDefaults; }); _registerModule(_modules, 'Stock/Scrollbar/Scrollbar.js', [_modules['Core/Defaults.js'], _modules['Core/Globals.js'], _modules['Core/Axis/ScrollbarAxis.js'], _modules['Stock/Scrollbar/ScrollbarDefaults.js'], _modules['Core/Utilities.js']], function (D, H, ScrollbarAxis, ScrollbarDefaults, U) { /* * * * (c) 2010-2024 Torstein Honsi * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ const { defaultOptions } = D; const { addEvent, correctFloat, defined, destroyObjectProperties, fireEvent, merge, pick, removeEvent } = U; /* * * * Constants * * */ /* eslint-disable no-invalid-this, valid-jsdoc */ /** * A reusable scrollbar, internally used in Highcharts Stock's * navigator and optionally on individual axes. * * @private * @class * @name Highcharts.Scrollbar * @param {Highcharts.SVGRenderer} renderer * @param {Highcharts.ScrollbarOptions} options * @param {Highcharts.Chart} chart */ class Scrollbar { /* * * * Static Functions * * */ static compose(AxisClass) { ScrollbarAxis.compose(AxisClass, Scrollbar); } /** * When we have vertical scrollbar, rifles and arrow in buttons should be * rotated. The same method is used in Navigator's handles, to rotate them. * * @function Highcharts.swapXY * * @param {Highcharts.SVGPathArray} path * Path to be rotated. * * @param {boolean} [vertical] * If vertical scrollbar, swap x-y values. * * @return {Highcharts.SVGPathArray} * Rotated path. * * @requires modules/stock */ static swapXY(path, vertical) { if (vertical) { path.forEach((seg) => { const len = seg.length; let temp; for (let i = 0; i < len; i += 2) { temp = seg[i + 1]; if (typeof temp === 'number') { seg[i + 1] = seg[i + 2]; seg[i + 2] = temp; } } }); } return path; } /* * * * Constructors * * */ constructor(renderer, options, chart) { /* * * * Properties * * */ this._events = []; this.chartX = 0; this.chartY = 0; this.from = 0; this.scrollbarButtons = []; this.scrollbarLeft = 0; this.scrollbarStrokeWidth = 1; this.scrollbarTop = 0; this.size = 0; this.to = 0; this.trackBorderWidth = 1; this.x = 0; this.y = 0; this.init(renderer, options, chart); } /* * * * Functions * * */ /** * Set up the mouse and touch events for the Scrollbar * * @private * @function Highcharts.Scrollbar#addEvents */ addEvents() { const buttonsOrder = this.options.inverted ? [1, 0] : [0, 1], buttons = this.scrollbarButtons, bar = this.scrollbarGroup.element, track = this.track.element, mouseDownHandler = this.mouseDownHandler.bind(this), mouseMoveHandler = this.mouseMoveHandler.bind(this), mouseUpHandler = this.mouseUpHandler.bind(this); const _events = [ // Mouse events [ buttons[buttonsOrder[0]].element, 'click', this.buttonToMinClick.bind(this) ], [ buttons[buttonsOrder[1]].element, 'click', this.buttonToMaxClick.bind(this) ], [track, 'click', this.trackClick.bind(this)], [bar, 'mousedown', mouseDownHandler], [bar.ownerDocument, 'mousemove', mouseMoveHandler], [bar.ownerDocument, 'mouseup', mouseUpHandler], // Touch events [bar, 'touchstart', mouseDownHandler], [bar.ownerDocument, 'touchmove', mouseMoveHandler], [bar.ownerDocument, 'touchend', mouseUpHandler] ]; // Add them all _events.forEach(function (args) { addEvent.apply(null, args); }); this._events = _events; } buttonToMaxClick(e) { const scroller = this; const range = ((scroller.to - scroller.from) * pick(scroller.options.step, 0.2)); scroller.updatePosition(scroller.from + range, scroller.to + range); fireEvent(scroller, 'changed', { from: scroller.from, to: scroller.to, trigger: 'scrollbar', DOMEvent: e }); } buttonToMinClick(e) { const scroller = this; const range = correctFloat(scroller.to - scroller.from) * pick(scroller.options.step, 0.2); scroller.updatePosition(correctFloat(scroller.from - range), correctFloat(scroller.to - range)); fireEvent(scroller, 'changed', { from: scroller.from, to: scroller.to, trigger: 'scrollbar', DOMEvent: e }); } /** * Get normalized (0-1) cursor position over the scrollbar * * @private * @function Highcharts.Scrollbar#cursorToScrollbarPosition * * @param {*} normalizedEvent * normalized event, with chartX and chartY values * * @return {Highcharts.Dictionary} * Local position {chartX, chartY} */ cursorToScrollbarPosition(normalizedEvent) { const scroller = this, options = scroller.options, minWidthDifference = options.minWidth > scroller.calculatedWidth ? options.minWidth : 0; // `minWidth` distorts translation return { chartX: (normalizedEvent.chartX - scroller.x - scroller.xOffset) / (scroller.barWidth - minWidthDifference), chartY: (normalizedEvent.chartY - scroller.y - scroller.yOffset) / (scroller.barWidth - minWidthDifference) }; } /** * Destroys allocated elements. * * @private * @function Highcharts.Scrollbar#destroy */ destroy() { const scroller = this, navigator = scroller.chart.scroller; // Disconnect events added in addEvents scroller.removeEvents(); // Destroy properties [ 'track', 'scrollbarRifles', 'scrollbar', 'scrollbarGroup', 'group' ].forEach(function (prop) { if (scroller[prop] && scroller[prop].destroy) { scroller[prop] = scroller[prop].destroy(); } }); // #6421, chart may have more scrollbars if (navigator && scroller === navigator.scrollbar) { navigator.scrollbar = null; // Destroy elements in collection destroyObjectProperties(navigator.scrollbarButtons); } } /** * Draw the scrollbar buttons with arrows * * @private * @function Highcharts.Scrollbar#drawScrollbarButton * @param {number} index * 0 is left, 1 is right */ drawScrollbarButton(index) { const scroller = this, renderer = scroller.renderer, scrollbarButtons = scroller.scrollbarButtons, options = scroller.options, size = scroller.size, group = renderer.g().add(scroller.group); scrollbarButtons.push(group); if (options.buttonsEnabled) { // Create a rectangle for the scrollbar button const rect = renderer.rect() .addClass('highcharts-scrollbar-button') .add(group); // Presentational attributes if (!scroller.chart.styledMode) { rect.attr({ stroke: options.buttonBorderColor, 'stroke-width': options.buttonBorderWidth, fill: options.buttonBackgroundColor }); } // Place the rectangle based on the rendered stroke width rect.attr(rect.crisp({ x: -0.5, y: -0.5, // +1 to compensate for crispifying in rect method width: size + 1, height: size + 1, r: options.buttonBorderRadius }, rect.strokeWidth())); // Button arrow const arrow = renderer .path(Scrollbar.swapXY([[ 'M', size / 2 + (index ? -1 : 1), size / 2 - 3 ], [ 'L', size / 2 + (index ? -1 : 1), size / 2 + 3 ], [ 'L', size / 2 + (index ? 2 : -2), size / 2 ]], options.vertical)) .addClass('highcharts-scrollbar-arrow') .add(scrollbarButtons[index]); if (!scroller.chart.styledMode) { arrow.attr({ fill: options.buttonArrowColor }); } } } /** * @private * @function Highcharts.Scrollbar#init * @param {Highcharts.SVGRenderer} renderer * @param {Highcharts.ScrollbarOptions} options * @param {Highcharts.Chart} chart */ init(renderer, options, chart) { const scroller = this; scroller.scrollbarButtons = []; scroller.renderer = renderer; scroller.userOptions = options; scroller.options = merge(ScrollbarDefaults, defaultOptions.scrollbar, options); scroller.options.margin = pick(scroller.options.margin, 10); scroller.chart = chart; // Backward compatibility scroller.size = pick(scroller.options.size, scroller.options.height); // Init if (options.enabled) { scroller.render(); scroller.addEvents(); } } mouseDownHandler(e) { const scroller = this, normalizedEvent = scroller.chart.pointer?.normalize(e) || e, mousePosition = scroller.cursorToScrollbarPosition(normalizedEvent); scroller.chartX = mousePosition.chartX; scroller.chartY = mousePosition.chartY; scroller.initPositions = [scroller.from, scroller.to]; scroller.grabbedCenter = true; } /** * Event handler for the mouse move event. * @private */ mouseMoveHandler(e) { const scroller = this, normalizedEvent = scroller.chart.pointer?.normalize(e) || e, options = scroller.options, direction = options.vertical ? 'chartY' : 'chartX', initPositions = scroller.initPositions || []; let scrollPosition, chartPosition, change; // 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 (scroller.grabbedCenter && // #4696, scrollbar failed on Android (!e.touches || e.touches[0][direction] !== 0)) { chartPosition = scroller.cursorToScrollbarPosition(normalizedEvent)[direction]; scrollPosition = scroller[direction]; change = chartPosition - scrollPosition; scroller.hasDragged = true; scroller.updatePosition(initPositions[0] + change, initPositions[1] + change); if (scroller.hasDragged) { fireEvent(scroller, 'changed', { from: scroller.from, to: scroller.to, trigger: 'scrollbar', DOMType: e.type, DOMEvent: e }); } } } /** * Event handler for the mouse up event. * @private */ mouseUpHandler(e) { const scroller = this; if (scroller.hasDragged) { fireEvent(scroller, 'changed', { from: scroller.from, to: scroller.to, trigger: 'scrollbar', DOMType: e.type, DOMEvent: e }); } scroller.grabbedCenter = scroller.hasDragged = scroller.chartX = scroller.chartY = null; } /** * Position the scrollbar, method called from a parent with defined * dimensions. * * @private * @function Highcharts.Scrollbar#position * @param {number} x * x-position on the chart * @param {number} y * y-position on the chart * @param {number} width * width of the scrollbar * @param {number} height * height of the scrollbar */ position(x, y, width, height) { const scroller = this, options = scroller.options, { buttonsEnabled, margin = 0, vertical } = options, method = scroller.rendered ? 'animate' : 'attr'; let xOffset = height, yOffset = 0; // Make the scrollbar visible when it is repositioned, #15763. scroller.group.show(); scroller.x = x; scroller.y = y + this.trackBorderWidth; scroller.width = width; // Width with buttons scroller.height = height; scroller.xOffset = xOffset; scroller.yOffset = yOffset; // If Scrollbar is a vertical type, swap options: if (vertical) { scroller.width = scroller.yOffset = width = yOffset = scroller.size; scroller.xOffset = xOffset = 0; scroller.yOffset = yOffset = buttonsEnabled ? scroller.size : 0; // Width without buttons scroller.barWidth = height - (buttonsEnabled ? width * 2 : 0); scroller.x = x = x + margin; } else { scroller.height = height = scroller.size; scroller.xOffset = xOffset = buttonsEnabled ? scroller.size : 0; // Width without buttons scroller.barWidth = width - (buttonsEnabled ? height * 2 : 0); scroller.y = scroller.y + margin; } // Set general position for a group: scroller.group[method]({ translateX: x, translateY: scroller.y }); // Resize background/track: scroller.track[method]({ width: width, height: height }); // Move right/bottom button to its place: scroller.scrollbarButtons[1][method]({ translateX: vertical ? 0 : width - xOffset, translateY: vertical ? height - yOffset : 0 }); } /** * Removes the event handlers attached previously with addEvents. * * @private * @function Highcharts.Scrollbar#removeEvents */ removeEvents() { this._events.forEach(function (args) { removeEvent.apply(null, args); }); this._events.length = 0; } /** * Render scrollbar with all required items. * * @private * @function Highcharts.Scrollbar#render */ render() { const scroller = this, renderer = scroller.renderer, options = scroller.options, size = scroller.size, styledMode = scroller.chart.styledMode, group = renderer.g('scrollbar') .attr({ zIndex: options.zIndex }) .hide() // Initially hide the scrollbar #15863 .add(); // Draw the scrollbar group scroller.group = group; // Draw the scrollbar track: scroller.track = renderer.rect() .addClass('highcharts-scrollbar-track') .attr({ r: options.trackBorderRadius || 0, height: size, width: size }).add(group); if (!styledMode) { scroller.track.attr({ fill: options.trackBackgroundColor, stroke: options.trackBorderColor, 'stroke-width': options.trackBorderWidth }); } const trackBorderWidth = scroller.trackBorderWidth = scroller.track.strokeWidth(); scroller.track.attr({ x: -trackBorderWidth % 2 / 2, y: -trackBorderWidth % 2 / 2 }); // Draw the scrollbar itself scroller.scrollbarGroup = renderer.g().add(group); scroller.scrollbar = renderer.rect() .addClass('highcharts-scrollbar-thumb') .attr({ height: size - trackBorderWidth, width: size - trackBorderWidth, r: options.barBorderRadius || 0 }).add(scroller.scrollbarGroup); scroller.scrollbarRifles = renderer .path(Scrollbar.swapXY([ ['M', -3, size / 4], ['L', -3, 2 * size / 3], ['M', 0, size / 4], ['L', 0, 2 * size / 3], ['M', 3, size / 4], ['L', 3, 2 * size / 3] ], options.vertical)) .addClass('highcharts-scrollbar-rifles') .add(scroller.scrollbarGroup); if (!styledMode) { scroller.scrollbar.attr({ fill: options.barBackgroundColor, stroke: options.barBorderColor, 'stroke-width': options.barBorderWidth }); scroller.scrollbarRifles.attr({ stroke: options.rifleColor, 'stroke-width': 1 }); } scroller.scrollbarStrokeWidth = scroller.scrollbar.strokeWidth(); scroller.scrollbarGroup.translate(-scroller.scrollbarStrokeWidth % 2 / 2, -scroller.scrollbarStrokeWidth % 2 / 2); // Draw the buttons: scroller.drawScrollbarButton(0); scroller.drawScrollbarButton(1); } /** * Set scrollbar size, with a given scale. * * @private * @function Highcharts.Scrollbar#setRange * @param {number} from * scale (0-1) where bar should start * @param {number} to * scale (0-1) where bar should end */ setRange(from, to) { const scroller = this, options = scroller.options, vertical = options.vertical, minWidth = options.minWidth, fullWidth = scroller.barWidth, method = (this.rendered && !this.hasDragged && !(this.chart.navigator && this.chart.navigator.hasDragged)) ? 'animate' : 'attr'; if (!defined(fullWidth)) { return; } const toPX = fullWidth * Math.min(to, 1); let fromPX, newSize; from = Math.max(from, 0); fromPX = Math.ceil(fullWidth * from); scroller.calculatedWidth = newSize = correctFloat(toPX - fromPX); // We need to recalculate position, if minWidth is used if (newSize < minWidth) { fromPX = (fullWidth - minWidth + newSize) * from; newSize = minWidth; } const newPos = Math.floor(fromPX + scroller.xOffset + scroller.yOffset); const newRiflesPos = newSize / 2 - 0.5; // -0.5 -> rifle line width / 2 // Store current position: scroller.from = from; scroller.to = to; if (!vertical) { scroller.scrollbarGroup[method]({ translateX: newPos }); scroller.scrollbar[method]({ width: newSize }); scroller.scrollbarRifles[method]({ translateX: newRiflesPos }); scroller.scrollbarLeft = newPos; scroller.scrollbarTop = 0; } else { scroller.scrollbarGroup[method]({ translateY: newPos }); scroller.scrollbar[method]({ height: newSize }); scroller.scrollbarRifles[method]({ translateY: newRiflesPos }); scroller.scrollbarTop = newPos; scroller.scrollbarLeft = 0; } if (newSize <= 12) { scroller.scrollbarRifles.hide(); } else { scroller.scrollbarRifles.show(); } // Show or hide the scrollbar based on the showFull setting if (options.showFull === false) { if (from <= 0 && to >= 1) { scroller.group.hide(); } else { scroller.group.show(); } } scroller.rendered = true; } /** * Checks if the extremes should be updated in response to a scrollbar * change event. * * @private * @function Highcharts.Scrollbar#shouldUpdateExtremes */ shouldUpdateExtremes(eventType) { return (pick(this.options.liveRedraw, H.svg && !H.isTouchDevice && !this.chart.boosted) || // Mouseup always should change extremes eventType === 'mouseup' || eventType === 'touchend' || // Internal events !defined(eventType)); } trackClick(e) { const scroller = this; const normalizedEvent = scroller.chart.pointer?.normalize(e) || e, range = scroller.to - scroller.from, top = scroller.y + scroller.scrollbarTop, left = scroller.x + scroller.scrollbarLeft; if ((scroller.options.vertical && normalizedEvent.chartY > top) || (!scroller.options.vertical && normalizedEvent.chartX > left)) { // On the top or on the left side of the track: scroller.updatePosition(scroller.from + range, scroller.to + range); } else { // On the bottom or the right side of the track: scroller.updatePosition(scroller.from - range, scroller.to - range); } fireEvent(scroller, 'changed', { from: scroller.from, to: scroller.to, trigger: 'scrollbar', DOMEvent: e }); } /** * Update the scrollbar with new options * * @private * @function Highcharts.Scrollbar#update * @param {Highcharts.ScrollbarOptions} options */ update(options) { this.destroy(); this.init(this.chart.renderer, merge(true, this.options, options), this.chart); } /** * Update position option in the Scrollbar, with normalized 0-1 scale * * @private * @function Highcharts.Scrollbar#updatePosition * @param {number} from * @param {number} to */ updatePosition(from, to) { if (to > 1) { from = correctFloat(1 - correctFloat(to - from)); to = 1; } if (from < 0) { to = correctFloat(to - from); from = 0; } this.from = from; this.to = to; } } /* * * * Static Properties * * */ Scrollbar.defaultOptions = ScrollbarDefaults; /* * * * Registry * * */ defaultOptions.scrollbar = merge(true, Scrollbar.defaultOptions, defaultOptions.scrollbar); /* * * * Default Export * * */ return Scrollbar; }); _registerModule(_modules, 'Stock/Navigator/Navigator.js', [_modules['Core/Axis/Axis.js'], _modules['Stock/Navigator/ChartNavigatorComposition.js'], _modules['Core/Defaults.js'], _modules['Core/Globals.js'], _modules['Core/Axis/NavigatorAxisComposition.js'], _modules['Stock/Navigator/NavigatorComposition.js'], _modules['Stock/Scrollbar/Scrollbar.js'], _modules['Core/Utilities.js']], function (Axis, ChartNavigatorComposition, D, H, NavigatorAxisAdditions, NavigatorComposition, Scrollbar, U) { /* * * * (c) 2010-2024 Torstein Honsi * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ const { defaultOptions } = D; const { isTouchDevice } = H; 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.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], ['M', left, zoomedMax], ['L', left + height, zoomedMax], [ '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, navigatorTop + halfOutline ] ]; 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 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 = 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 = 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); } } navigator.shades[index] = shade; }); // Create the 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 && navigatorOptions.handles.enabled) { const handlesOptions = navigatorOptions.handles, { height, width } = handlesOptions; [0, 1].forEach((index) => { navigator.handles[index] = renderer.symbol(handlesOptions.symbols[index], -width / 2 - 1, 0, width, height, handlesOptions); if (chart.inverted) { navigator.handles[index].attr({ rotation: 90, rotationOriginX: Math.floor(-width / 2), rotationOriginY: (height + width) / 2 }); } // 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); if (!chart.styledMode) { navigator.handles[index] .attr({ fill: handlesOptions.backgroundColor, stroke: handlesOptions.borderColor, 'stroke-width': handlesOptions.lineWidth }) .css(mouseCursor); } }); } } /** * Update navigator * * @private * @function Highcharts.Navigator#update * * @param {Highcharts.NavigatorOptions} options * Options to merge in when updating navigator */ update(options) { // Remove references to old navigator series in base series (this.series || []).forEach((series) => { if (series.baseSeries) { delete series.baseSeries.navigatorSeries; } }); // Destroy and rebuild navigator this.destroy(); const chartOptions = this.chart.options; merge(true, chartOptions.navigator, options); this.init(this.chart); } /** * 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; } 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; 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} * 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); } } } /** * 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.opposite = pick(navigatorOptions.opposite, Boolean(!navigatorEnabled && chart.inverted)); // #6262 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) { // 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, { id: 'navigator-x-axis', yAxis: 'navigator-y-axis', type: 'datetime', index: xAxisIndex, isInternal: true, offset: 0, keepOrdinalPadding: true, startOnTick: false, endOnTick: false, minPadding: 0, maxPadding: 0, zoomEnabled: false }, chart.inverted ? { offsets: [scrollButtonSize, 0, -scrollButtonSize, 0], width: height } : { offsets: [0, -scrollButtonSize, 0, scrollButtonSize], height: height }), 'xAxis'); navigator.yAxis = new Axis(chart, merge(navigatorOptions.yAxis, { id: 'navigator-y-axis', alignTicks: false, offset: 0, index: yAxisIndex, isInternal: true, reversed: pick((navigatorOptions.yAxis && navigatorOptions.yAxis.reversed), (chart.yAxis[0] && chart.yAxis[0].reversed), false), 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) && navigator.navigatorEnabled) { 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(); } /** * 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], navAxis = this.xAxis, navAxisOptions = navAxis.options, baseAxisOptions = baseAxis.options; let ret; if (!returnFalseOnNoBaseSeries || baseAxis.dataMin !== null) { ret = { dataMin: pick(// #4053 navAxisOptions && navAxisOptions.min, numExt('min', baseAxisOptions.min, baseAxis.dataMin, navAxis.dataMin, navAxis.min)), dataMax: pick(navAxisOptions && navAxisOptions.max, numExt('max', 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, linkedTo: null, group: 'nav', padXAxis: false, xAxis: 'navigator-x-axis', yAxis: 'navigator-y-axis', showInLegend: false, stacking: void 0, 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 && 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 = 'Navigator ' + (navigatorSeries.length + 1); mergedNavSeriesOptions = merge(defaultOptions.navigator.series, { // Since we don't have a base series to pull color from, // try to fake it by using color from series with same // index. Otherwise pull from the colors array. We need // an explicit color as otherwise updates will increment // color counter and we'll get a new color for each // update of the nav series. color: chart.series[i] && !chart.series[i].options.isInternal && chart.series[i].color || chart.options.colors[i] || chart.options.colors[0] }, navSeriesMixin, userSeriesOptions); mergedNavSeriesOptions.data = userSeriesOptions.data; if (mergedNavSeriesOptions.data) { navigator.hasNavigatorData = true; navigatorSeries.push(chart.initSeries(mergedNavSeriesOptions)); } }); } if (addEvents) { this.addBaseSeriesEvents(); } } /** * Add data events. * For example when main series is updated we need to recalculate extremes * * @private * @function Highcharts.Navigator#addBaseSeriesEvent */ addBaseSeriesEvents() { const navigator = this, baseSeries = navigator.baseSeries || []; // Bind modified extremes event to first base's xAxis only. // In event of > 1 base-xAxes, the navigator will ignore those. // Adding this multiple times to the same axis is no problem, as // duplicates should be discarded by the browser. if (baseSeries[0] && baseSeries[0].xAxis) { baseSeries[0].eventsToUnbind.push(addEvent(baseSeries[0].xAxis, 'foundExtremes', this.modifyBaseAxisExtremes)); } baseSeries.forEach((base) => { // Link base series show/hide to navigator series visibility base.eventsToUnbind.push(addEvent(base, 'show', function () { if (this.navigatorSeries) { this.navigatorSeries.setVisible(true, false); } })); base.eventsToUnbind.push(addEvent(base, 'hide', function () { if (this.navigatorSeries) { this.navigatorSeries.setVisible(false, false); } })); // Respond to updated data in the base series, unless explicitly // not adapting to data changes. if (this.navigatorOptions.adaptToUpdatedData !== false) { if (base.xAxis) { base.eventsToUnbind.push(addEvent(base, 'updatedData', this.updatedDataHandler)); } } // Handle series removal base.eventsToUnbind.push(addEvent(base, 'remove', function () { if (this.navigatorSeries) { erase(navigator.series, this.navigatorSeries); if (defined(this.navigatorSeries.options)) { this.navigatorSeries.remove(false); } delete this.navigatorSeries; } })); }); } /** * Get minimum from all base series connected to the navigator * @private * @param {number} currentSeriesMin * Minium from the current series * @return {number} * Minimum from all series */ getBaseSeriesMin(currentSeriesMin) { return this.baseSeries.reduce(function (min, series) { // (#10193) return Math.min(min, series.xData && series.xData.length ? series.xData[0] : min); }, currentSeriesMin); } /** * Set the navigator x axis extremes to reflect the total. The navigator * extremes should always be the extremes of the union of all series in the * chart as well as the navigator series. * * @private * @function Highcharts.Navigator#modifyNavigatorAxisExtremes */ modifyNavigatorAxisExtremes() { const xAxis = this.xAxis; if (typeof xAxis.getExtremes !== 'undefined') { const unionExtremes = this.getUnionExtremes(true); if (unionExtremes && (unionExtremes.dataMin !== xAxis.min || unionExtremes.dataMax !== xAxis.max)) { xAxis.min = unionExtremes.dataMin; xAxis.max = unionExtremes.dataMax; } } } /** * Hook to modify the base axis extremes with information from the Navigator * * @private * @function Highcharts.Navigator#modifyBaseAxisExtremes */ modifyBaseAxisExtremes() { const baseXAxis = this, navigator = baseXAxis.chart.navigator, baseExtremes = baseXAxis.getExtremes(), baseMin = baseExtremes.min, baseMax = baseExtremes.max, baseDataMin = baseExtremes.dataMin, baseDataMax = baseExtremes.dataMax, range = baseMax - baseMin, stickToMin = navigator.stickToMin, stickToMax = navigator.stickToMax, overscroll = pick(baseXAxis.ordinal?.convertOverscroll(baseXAxis.options.overscroll), 0), navigatorSeries = navigator.series && navigator.series[0], hasSetExtremes = !!baseXAxis.setExtremes, // When the extremes have been set by range selector button, don't // stick to min or max. The range selector buttons will handle the // extremes. (#5489) unmutable = baseXAxis.eventArgs && baseXAxis.eventArgs.trigger === 'rangeSelectorButton'; let newMax, newMin; if (!unmutable) { // If the zoomed range is already at the min, move it to the right // as new data comes in if (stickToMin) { newMin = baseDataMin; newMax = newMin + range; } // If the zoomed range is already at the max, move it to the right // as new data comes in if (stickToMax) { newMax = baseDataMax + overscroll; // If stickToMin is true, the new min value is set above if (!stickToMin) { newMin = Math.max(baseDataMin, // Don't go below data extremes (#13184) newMax - range, navigator.getBaseSeriesMin(navigatorSeries && navigatorSeries.xData ? navigatorSeries.xData[0] : -Number.MAX_VALUE)); } } // Update the extremes if (hasSetExtremes && (stickToMin || stickToMax)) { if (isNumber(newMin)) { baseXAxis.min = baseXAxis.userMin = newMin; baseXAxis.max = baseXAxis.userMax = newMax; } } } // Reset navigator.stickToMin = navigator.stickToMax = null; } /** * Handler for updated data on the base series. When data is modified, the * navigator series must reflect it. This is called from the Chart.redraw * function before axis and series extremes are computed. * * @private * @function Highcharts.Navigator#updateDataHandler */ updatedDataHandler() { const navigator = this.chart.navigator, baseSeries = this, navigatorSeries = this.navigatorSeries, shouldStickToMax = navigator.reversedExtremes ? Math.round(navigator.zoomedMin) === 0 : Math.round(navigator.zoomedMax) >= Math.round(navigator.size); // If the scrollbar is scrolled all the way to the right, keep right as // new data comes in, unless user set navigator.stickToMax to false. navigator.stickToMax = pick(this.chart.options.navigator && this.chart.options.navigator.stickToMax, shouldStickToMax); navigator.stickToMin = navigator.shouldStickToMin(baseSeries, navigator); // Set the navigator series data to the new data of the base series if (navigatorSeries && !navigator.hasNavigatorData) { navigatorSeries.options.pointStart = baseSeries.xData[0]; navigatorSeries.setData(baseSeries.options.data, false, null, false); // #5414 } } /** * Detect if the zoomed area should stick to the minimum, #14742. * * @private * @function Highcharts.Navigator#shouldStickToMin */ shouldStickToMin(baseSeries, navigator) { const xDataMin = navigator.getBaseSeriesMin(baseSeries.xData[0]), xAxis = baseSeries.xAxis, max = xAxis.max, min = xAxis.min, range = xAxis.options.range; let stickToMin = true; if (isNumber(max) && isNumber(min)) { // If range declared, stick to the minimum only if the range // is smaller than the data set range. if (range && max - xDataMin > 0) { stickToMin = max - xDataMin < range; } else { // If the current axis minimum falls outside the new // updated dataset, we must adjust. stickToMin = min <= xDataMin; } } else { stickToMin = false; // #15864 } return stickToMin; } /** * Add chart events, like redrawing navigator, when chart requires that. * * @private * @function Highcharts.Navigator#addChartEvents */ addChartEvents() { if (!this.eventsToUnbind) { this.eventsToUnbind = []; } this.eventsToUnbind.push( // Move the scrollbar after redraw, like after data updata even if // axes don't redraw addEvent(this.chart, 'redraw', function () { const navigator = this.navigator, xAxis = navigator && (navigator.baseSeries && navigator.baseSeries[0] && navigator.baseSeries[0].xAxis || this.xAxis[0]); // #5709, #13114 if (xAxis) { navigator.render(xAxis.min, xAxis.max); } }), // Make room for the navigator, can be placed around the chart: addEvent(this.chart, 'getMargins', function () { const chart = this, navigator = chart.navigator; let marginName = navigator.opposite ? 'plotTop' : 'marginBottom'; if (chart.inverted) { marginName = navigator.opposite ? 'marginRight' : 'plotLeft'; } chart[marginName] = (chart[marginName] || 0) + (navigator.navigatorEnabled || !chart.inverted ? navigator.height + navigator.scrollbarHeight : 0) + navigator.navigatorOptions.margin; }), addEvent(Navigator, 'setRange', function (e) { this.chart.xAxis[0].setExtremes(e.min, e.max, e.redraw, e.animation, e.eventArguments); })); } /** * Destroys allocated elements. * * @private * @function Highcharts.Navigator#destroy */ destroy() { // Disconnect events added in addEvents this.removeEvents(); if (this.xAxis) { erase(this.chart.xAxis, this.xAxis); erase(this.chart.axes, this.xAxis); } if (this.yAxis) { erase(this.chart.yAxis, this.yAxis); erase(this.chart.axes, this.yAxis); } // Destroy series (this.series || []).forEach((s) => { if (s.destroy) { s.destroy(); } }); // Destroy properties [ 'series', 'xAxis', 'yAxis', 'shades', 'outline', 'scrollbarTrack', 'scrollbarRifles', 'scrollbarGroup', 'scrollbar', 'navigatorGroup', 'rendered' ].forEach((prop) => { if (this[prop] && this[prop].destroy) { this[prop].destroy(); } this[prop] = null; }); // Destroy elements in collection [this.handles].forEach((coll) => { destroyObjectProperties(coll); }); } } /* * * * Default Export * * */ return Navigator; }); _registerModule(_modules, 'Stock/Navigator/StandaloneNavigatorDefaults.js', [], function () { /* * * * (c) 2010-2024 Mateusz Bernacik * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ /* * * * Constants * * */ const standaloneNavigatorDefaults = { chart: { height: 70, margin: [0, 5, 0, 5] }, exporting: { enabled: false }, legend: { enabled: false }, navigator: { enabled: false }, plotOptions: { series: { states: { hover: { enabled: false } }, marker: { enabled: false } } }, scrollbar: { enabled: false }, title: { text: '' }, tooltip: { enabled: false }, xAxis: { visible: false }, yAxis: { height: 0, visible: false } }; /* * * * Default Export * * */ return standaloneNavigatorDefaults; }); _registerModule(_modules, 'Stock/Navigator/StandaloneNavigator.js', [_modules['Core/Chart/Chart.js'], _modules['Stock/Navigator/Navigator.js'], _modules['Core/Globals.js'], _modules['Core/Utilities.js'], _modules['Core/Axis/Axis.js'], _modules['Stock/Navigator/StandaloneNavigatorDefaults.js']], function (Chart, Navigator, G, U, Axis, standaloneNavigatorDefaults) { /* * * * (c) 2010-2024 Mateusz Bernacik * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ const { merge, addEvent, fireEvent, pick } = U; /* * * * Class * * */ /** * The StandaloneNavigator class. The StandaloneNavigator class allows for * creating a standalone navigator component that synchronizes the extremes * across multiple bound charts. * * @class * @name Highcharts.StandaloneNavigator * * @param {string|Highcharts.HTMLDOMElement} [renderTo] * The DOM element to render to, or its id. * * @param {StandaloneNavigatorOptions} userOptions * The standalone navigator options. */ class StandaloneNavigator { /* * * * Static Functions * * */ /** * Factory function for standalone navigator. * * @function Highcharts.navigator * * @param {string|Highcharts.HTMLDOMElement} [renderTo] * The DOM element to render to, or its id. * * @param {StandaloneNavigatorOptions} options * The standalone navigator options with chart-like structure. * * Returns the navigator object. */ static navigator(renderTo, options) { const nav = new StandaloneNavigator(renderTo, options); if (!G.navigators) { G.navigators = [nav]; } else { G.navigators.push(nav); } return nav; } /* * * * Constructor * * */ constructor(element, userOptions) { this.boundAxes = []; this.userOptions = userOptions; this.chartOptions = merge(G.getOptions(), standaloneNavigatorDefaults, { navigator: userOptions }); const chart = new Chart(element, this.chartOptions); chart.options = merge(chart.options, { navigator: { enabled: true }, scrollbar: { enabled: true } }); if (this.chartOptions.navigator && this.chartOptions.scrollbar) { this.chartOptions.navigator.enabled = true; this.chartOptions.scrollbar.enabled = true; } this.navigator = new Navigator(chart); chart.navigator = this.navigator; this.initNavigator(); } /** * Binds an axis to the standalone navigator, * allowing the navigator to control the axis' range. * * @sample stock/standalone-navigator/bind/ * Bind chart with a button * * @function Highcharts.StandaloneNavigator#bind * * @param {Axis | Chart} axisOrChart * The Axis or Chart to bind to the navigator. * * @param {boolean} [twoWay=true] * Enables two-way binding between the navigator and the axis/chart. * When true, changes in the navigator's range will update the axis * and vice versa. When false, changes in the navigator's range will * be reflected in the axis, but changes in the axis ranges won't be * reflected on the navigator. */ bind(axisOrChart, twoWay = true) { const nav = this; // If the chart is passed, bind the first xAxis const axis = (axisOrChart instanceof Chart) ? axisOrChart.xAxis[0] : axisOrChart; if (!(axis instanceof Axis)) { return; } const { min, max } = this.navigator.xAxis, removeEventCallbacks = []; if (twoWay) { const removeSetExtremesEvent = addEvent(axis, 'setExtremes', (e) => { if (e.trigger === 'pan' || e.trigger === 'zoom' || e.trigger === 'mouseWheelZoom') { nav.setRange(e.min, e.max, true, e.trigger !== 'pan', { trigger: axis }); } }); removeEventCallbacks.push(removeSetExtremesEvent); } const removeSetRangeEvent = addEvent(this.navigator, 'setRange', (e) => { axis.setExtremes(e.min, e.max, e.redraw, e.animation); }); removeEventCallbacks.push(removeSetRangeEvent); let boundAxis = this.boundAxes.filter(function (boundAxis) { return boundAxis.axis === axis; })[0]; if (!boundAxis) { boundAxis = { axis, callbacks: [] }; this.boundAxes.push(boundAxis); } boundAxis.callbacks = removeEventCallbacks; // Show axis' series in navigator based on showInNavigator property axis.series.forEach((series) => { if (series.options.showInNavigator) { nav.addSeries(series.options); } }); // Set extremes to match the navigator's extremes axis.setExtremes(min, max); // Unbind the axis before it's destroyed addEvent(axis, 'destroy', (e) => { if (!e.keepEvents) { this.unbind(axis); } }); } /** * Unbinds a single axis or all bound axes from the standalone navigator. * * @sample stock/standalone-navigator/unbind/ * Unbind chart with a button * * @function Highcharts.StandaloneNavigator#unbind * * @param {Chart | Axis | undefined} axisOrChart * Passing a Chart object unbinds the first X axis of the chart, * an Axis object unbinds that specific axis, * and undefined unbinds all axes bound to the navigator. */ unbind(axisOrChart) { // If no axis or chart is provided, unbind all bound axes if (!axisOrChart) { this.boundAxes.forEach(({ callbacks }) => { callbacks.forEach((removeCallback) => removeCallback()); }); this.boundAxes.length = 0; return; } const axis = (axisOrChart instanceof Axis) ? axisOrChart : axisOrChart.xAxis[0]; for (let i = this.boundAxes.length - 1; i >= 0; i--) { if (this.boundAxes[i].axis === axis) { this.boundAxes[i].callbacks.forEach((callback) => callback()); this.boundAxes.splice(i, 1); } } } /** * Destroys allocated standalone navigator elements. * * @function Highcharts.StandaloneNavigator#destroy */ destroy() { // Disconnect events this.boundAxes.forEach(({ callbacks }) => { callbacks.forEach((removeCallback) => removeCallback()); }); this.boundAxes.length = 0; this.navigator.destroy(); this.navigator.chart.destroy(); } /** * Updates the standalone navigator's options with a new set of user * options. * * @sample stock/standalone-navigator/update/ * Bind chart with a button * * @function Highcharts.StandaloneNavigator#update * * @param {StandaloneNavigatorOptions} newOptions * Updates the standalone navigator's options with new user options. * * @param {boolean | undefined} redraw * Whether to redraw the standalone navigator. By default, if not * specified, the standalone navigator will be redrawn. */ update(newOptions, redraw) { this.chartOptions = merge(this.chartOptions, { navigator: newOptions }); this.navigator.chart.update(this.chartOptions, redraw); } /** * Redraws the standalone navigator. * * @function Highcharts.StandaloneNavigator#redraw */ redraw() { this.navigator.chart.redraw(); } /** * Adds a series to the standalone navigator. * * @private * * @param {SeriesOptions} seriesOptions * Options for the series to be added to the navigator. */ addSeries(seriesOptions) { this.navigator.chart.addSeries(merge(seriesOptions, { showInNavigator: pick(seriesOptions.showInNavigator, true) })); this.navigator.setBaseSeries(); } /** * Initialize the standalone navigator. * * @private */ initNavigator() { const nav = this.navigator; nav.top = 1; nav.xAxis.setScale(); nav.yAxis.setScale(); nav.xAxis.render(); nav.yAxis.render(); nav.series?.forEach((s) => { s.translate(); s.render(); s.redraw(); }); const { min, max } = this.getInitialExtremes(); nav.chart.xAxis[0].userMin = min; nav.chart.xAxis[0].userMax = max; nav.render(min, max); } /** * Get the current range of the standalone navigator. * * @sample stock/standalone-navigator/getrange/ * Report the standalone navigator's range by clicking on a button * * @function Highcharts.StandaloneNavigator#getRange * * @return {Highcharts.ExtremesObject} * The current range of the standalone navigator. */ getRange() { const { min, max } = this.navigator.chart.xAxis[0].getExtremes(), { userMin, userMax, min: dataMin, max: dataMax } = this.navigator.xAxis.getExtremes(); return { min: pick(min, dataMin), max: pick(max, dataMax), dataMin, dataMax, userMin, userMax }; } /** * Set the range of the standalone navigator. * * @sample stock/standalone-navigator/setrange/ * Set range from a button * * @function Highcharts.StandaloneNavigator#setRange * * @param {number | undefined} min * The new minimum value. * * @param {number | undefined} max * The new maximum value. * * @emits Highcharts.StandaloneNavigator#event:setRange */ setRange(min, max, redraw, animation, eventArguments) { fireEvent(this.navigator, 'setRange', { min, max, redraw, animation, eventArguments: merge(eventArguments, { trigger: 'navigator' }) }); } /** * Get the initial, options based extremes for the standalone navigator. * * @private * * @return {{ min: number, max: number }} * The initial minimum and maximum extremes values. */ getInitialExtremes() { const { min, max } = this.navigator.xAxis.getExtremes(); return { min: min, max: max }; } } /* * * * API Declarations * * */ /** * Standalone Navigator options. * * @interface Highcharts.StandaloneNavigatorOptions */ /** */ ''; // Detach doclets above return StandaloneNavigator; }); _registerModule(_modules, 'masters/modules/navigator.src.js', [_modules['Core/Globals.js'], _modules['Stock/Navigator/StandaloneNavigator.js'], _modules['Stock/Navigator/NavigatorComposition.js']], function (Highcharts, StandaloneNavigator, NavigatorComposition) { const G = Highcharts; G.StandaloneNavigator = G.StandaloneNavigator || StandaloneNavigator; G.navigator = G.StandaloneNavigator.navigator; NavigatorComposition.compose(G.Chart, G.Axis, G.Series); return Highcharts; }); }));