/* * * * (c) 2010-2020 Torstein Honsi * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ 'use strict'; import Axis from './Axis.js'; import Color from './Color.js'; var color = Color.parse; import H from './Globals.js'; import NavigatorAxis from './NavigatorAxis.js'; import Scrollbar from './Scrollbar.js'; import U from './Utilities.js'; var addEvent = U.addEvent, clamp = U.clamp, correctFloat = U.correctFloat, defined = U.defined, destroyObjectProperties = U.destroyObjectProperties, erase = U.erase, extend = U.extend, find = U.find, isArray = U.isArray, isNumber = U.isNumber, merge = U.merge, pick = U.pick, removeEvent = U.removeEvent, splat = U.splat; import './Chart.js'; import './Series.js'; import './Options.js'; var Chart = H.Chart, defaultOptions = H.defaultOptions, hasTouch = H.hasTouch, isTouchDevice = H.isTouchDevice, Series = H.Series, seriesTypes = H.seriesTypes, defaultSeriesType, // 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. numExt = function (extreme) { var args = []; for (var _i = 1; _i < arguments.length; _i++) { args[_i - 1] = arguments[_i]; } var numbers = [].filter.call(args, isNumber); if (numbers.length) { return Math[extreme].apply(0, numbers); } }; defaultSeriesType = typeof seriesTypes.areaspline === 'undefined' ? 'line' : 'areaspline'; extend(defaultOptions, { /** * 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 */ /** * 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 */ navigator: { /** * 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 Highstock 1.x it was always `false`. * * @sample {highstock} stock/navigator/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', /** * The stroke for the handle border and the stripes inside. * * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject} */ borderColor: '#999999' }, /** * 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('#6685c2').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: '#cccccc', /** * 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. Defaults to `areaspline` if * defined, otherwise `line`. * * 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 areaspline */ type: defaultSeriesType, /** * The fill opacity of the navigator series. */ fillOpacity: 0.05, /** * The pixel line width of the navigator series. */ lineWidth: 1, /** * @ignore-option */ compare: null, /** * 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, smoothed: true, // 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 Highstock v8, default value is the same as default * `pointRange` defined for a specific type (e.g. `null` for * column type). * * In Highstock 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 }, /** * 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 value is set in milliseconds. * Can be set for both, main xAxis and navigator's xAxis. * * @since 6.0.0 */ overscroll: 0, className: 'highcharts-navigator-xaxis', tickLength: 0, lineWidth: 0, gridLineColor: '#e6e6e6', gridLineWidth: 1, tickPixelInterval: 200, labels: { align: 'left', /** * @type {Highcharts.CSSObject} */ style: { /** @ignore */ color: '#999999' }, 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 } } }); /* eslint-disable no-invalid-this, valid-jsdoc */ /** * Draw one of the handles on the side of the zoomed range in the navigator * * @private * @function Highcharts.Renderer#symbols.navigator-handle * @param {number} x * @param {number} y * @param {number} w * @param {number} h * @param {Highcharts.NavigatorHandlesOptions} options * @return {Highcharts.SVGPathArray} * Path to be used in a handle */ H.Renderer.prototype.symbols['navigator-handle'] = function (x, y, w, h, options) { var halfWidth = options.width / 2, markerPosition = Math.round(halfWidth / 3) + 0.5, height = options.height || 0; 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] ]; }; /** * The Navigator class * * @private * @class * @name Highcharts.Navigator * * @param {Highcharts.Chart} chart * Chart object */ var Navigator = /** @class */ (function () { function Navigator(chart) { this.baseSeries = void 0; this.chart = void 0; this.handles = void 0; this.height = void 0; this.left = void 0; this.navigatorEnabled = void 0; this.navigatorGroup = void 0; this.navigatorOptions = void 0; this.navigatorSeries = void 0; this.navigatorSize = void 0; this.opposite = void 0; this.outline = void 0; this.outlineHeight = void 0; this.range = void 0; this.rendered = void 0; this.shades = void 0; this.size = void 0; this.top = void 0; this.xAxis = void 0; this.yAxis = void 0; this.zoomedMax = void 0; this.zoomedMin = void 0; this.init(chart); } /** * 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' */ Navigator.prototype.drawHandle = function (x, index, inverted, verb) { var 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' */ Navigator.prototype.drawOutline = function (zoomedMin, zoomedMax, inverted, verb) { var navigator = this, maskInside = navigator.navigatorOptions.maskInside, outlineWidth = navigator.outline.strokeWidth(), halfOutline = outlineWidth / 2, outlineCorrection = (outlineWidth % 2) / 2, // #5800 outlineHeight = navigator.outlineHeight, scrollbarHeight = navigator.scrollbarHeight || 0, navigatorSize = navigator.size, left = navigator.left - scrollbarHeight, navigatorTop = navigator.top, verticalMin, path; if (inverted) { left -= halfOutline; verticalMin = navigatorTop + zoomedMax + outlineCorrection; zoomedMax = navigatorTop + zoomedMin + outlineCorrection; path = [ ['M', left + outlineHeight, navigatorTop - scrollbarHeight - outlineCorrection], ['L', left + outlineHeight, verticalMin], ['L', left, verticalMin], ['L', left, zoomedMax], ['L', left + outlineHeight, zoomedMax], ['L', left + outlineHeight, navigatorTop + navigatorSize + scrollbarHeight] ]; if (maskInside) { path.push(['M', left + outlineHeight, verticalMin - halfOutline], // upper left of zoomed range ['L', left + outlineHeight, zoomedMax + halfOutline] // upper right of z.r. ); } } else { zoomedMin += left + scrollbarHeight - outlineCorrection; zoomedMax += left + scrollbarHeight - outlineCorrection; navigatorTop += halfOutline; path = [ ['M', left, navigatorTop], ['L', zoomedMin, navigatorTop], ['L', zoomedMin, navigatorTop + outlineHeight], ['L', zoomedMax, navigatorTop + outlineHeight], ['L', zoomedMax, navigatorTop], ['L', left + navigatorSize + scrollbarHeight * 2, navigatorTop] // right ]; if (maskInside) { path.push(['M', zoomedMin - halfOutline, navigatorTop], // upper left of zoomed range ['L', zoomedMax + halfOutline, navigatorTop] // upper right of z.r. ); } } 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' */ Navigator.prototype.drawMasks = function (zoomedMin, zoomedMax, inverted, verb) { var navigator = this, left = navigator.left, top = navigator.top, navigatorHeight = navigator.height, 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(function (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 */ Navigator.prototype.renderElements = function () { var navigator = this, navigatorOptions = navigator.navigatorOptions, maskInside = navigatorOptions.maskInside, chart = navigator.chart, inverted = chart.inverted, renderer = chart.renderer, navigatorGroup, mouseCursor = { cursor: inverted ? 'ns-resize' : 'ew-resize' }; // Create the main navigator group navigator.navigatorGroup = navigatorGroup = renderer.g('navigator') .attr({ zIndex: 8, visibility: 'hidden' }) .add(); // Create masks, each mask will get events and fill: [ !maskInside, maskInside, !maskInside ].forEach(function (hasMask, index) { navigator.shades[index] = renderer.rect() .addClass('highcharts-navigator-mask' + (index === 1 ? '-inside' : '-outside')) .add(navigatorGroup); if (!chart.styledMode) { navigator.shades[index] .attr({ fill: hasMask ? navigatorOptions.maskFill : 'rgba(0,0,0,0)' }) .css((index === 1) && mouseCursor); } }); // 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.enabled) { [0, 1].forEach(function (index) { navigatorOptions.handles.inverted = chart.inverted; navigator.handles[index] = renderer.symbol(navigatorOptions.handles.symbols[index], -navigatorOptions.handles.width / 2 - 1, 0, navigatorOptions.handles.width, navigatorOptions.handles.height, navigatorOptions.handles); // zIndex = 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) { var handlesOptions = navigatorOptions.handles; 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 */ Navigator.prototype.update = function (options) { // Remove references to old navigator series in base series (this.series || []).forEach(function (series) { if (series.baseSeries) { delete series.baseSeries.navigatorSeries; } }); // Destroy and rebuild navigator this.destroy(); var chartOptions = this.chart.options; merge(true, chartOptions.navigator, this.options, 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 * @return {void} */ Navigator.prototype.render = function (min, max, pxMin, pxMax) { var navigator = this, chart = navigator.chart, navigatorWidth, scrollbarLeft, scrollbarTop, scrollbarHeight = navigator.scrollbarHeight, navigatorSize, xAxis = navigator.xAxis, pointRange = xAxis.pointRange || 0, scrollbarXAxis = xAxis.navigatorAxis.fake ? chart.xAxis[0] : xAxis, navigatorEnabled = navigator.navigatorEnabled, zoomedMin, zoomedMax, rendered = navigator.rendered, inverted = chart.inverted, verb, newMin, newMax, currentRange, minRange = chart.xAxis[0].minRange, maxRange = chart.xAxis[0].options.maxRange; // 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 + scrollbarHeight + (inverted ? chart.plotWidth : 0)); navigator.size = zoomedMax = navigatorSize = pick(xAxis.len, (inverted ? chart.plotHeight : chart.plotWidth) - 2 * scrollbarHeight); if (inverted) { navigatorWidth = scrollbarHeight; } else { navigatorWidth = navigatorSize + 2 * scrollbarHeight; } // 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) 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); zoomedMin = Math.round(navigator.zoomedMin); if (navigatorEnabled) { navigator.navigatorGroup.attr({ visibility: 'visible' }); // 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 - scrollbarHeight; 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 * scrollbarHeight; } else { scrollbarTop = navigator.top + (navigatorEnabled ? navigator.height : -scrollbarHeight); scrollbarLeft = navigator.left - scrollbarHeight; } // 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; }; /** * Set up the mouse and touch events for the navigator * * @private * @function Highcharts.Navigator#addMouseEvents */ Navigator.prototype.addMouseEvents = function () { var navigator = this, chart = navigator.chart, container = chart.container, 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'); // Add mouse move and mouseup events. These are bind to doc/container, // because Navigator.grabbedSomething flags are stored in mousedown // events eventsToUnbind.push(addEvent(chart.renderTo, 'mousemove', mouseMoveHandler), addEvent(container.ownerDocument, 'mouseup', mouseUpHandler)); // Touch events if (hasTouch) { eventsToUnbind.push(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. */ Navigator.prototype.getPartsEvents = function (eventName) { var 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 */ Navigator.prototype.shadesMousedown = function (e, index) { e = this.chart.pointer.normalize(e); var navigator = this, chart = navigator.chart, xAxis = navigator.xAxis, zoomedMin = navigator.zoomedMin, navigatorPosition = navigator.left, navigatorSize = navigator.size, range = navigator.range, 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 chart.xAxis[0].setExtremes(Math.min(ext.min, ext.max), Math.max(ext.min, ext.max), true, null, // auto animation { 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 * @return {void} */ Navigator.prototype.handlesMousedown = function (e, index) { e = this.chart.pointer.normalize(e); var 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.fixedRange = null; }; /** * Mouse move event based on x/y mouse position. * * @private * @function Highcharts.Navigator#onMouseMove * * @param {Highcharts.PointerEventObject} e * Mouse event */ Navigator.prototype.onMouseMove = function (e) { var navigator = this, chart = navigator.chart, left = navigator.left, navigatorSize = navigator.navigatorSize, range = navigator.range, dragOffset = navigator.dragOffset, inverted = chart.inverted, 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); 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 VML, on touch // devices or if the chart is in boost. H.svg && !isTouchDevice && !this.chart.isBoosting)) { e.DOMType = e.type; // DOMType is for IE8 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 * @return {void} */ Navigator.prototype.onMouseUp = function (e) { var 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', zoomedMax = Math.round(navigator.zoomedMax), zoomedMin = Math.round(navigator.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)) { chart.xAxis[0].setExtremes(Math.min(ext.min, ext.max), Math.max(ext.min, ext.max), true, // Run animation when clicking buttons, scrollbar track etc, // but not when dragging handles or scrollbar navigator.hasDragged ? false : null, { 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) { 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 * @return {void} */ Navigator.prototype.removeEvents = function () { if (this.eventsToUnbind) { this.eventsToUnbind.forEach(function (unbind) { unbind(); }); this.eventsToUnbind = void 0; } this.removeBaseSeriesEvents(); }; /** * Remove data events. * * @private * @function Highcharts.Navigator#removeBaseSeriesEvents * @return {void} */ Navigator.prototype.removeBaseSeriesEvents = function () { var 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 * * @param {Highcharts.Chart} chart */ Navigator.prototype.init = function (chart) { var 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; this.handles = []; this.shades = []; this.chart = chart; this.setBaseSeries(); this.height = height; this.scrollbarHeight = scrollbarHeight; this.scrollbarEnabled = scrollbarEnabled; this.navigatorEnabled = navigatorEnabled; this.navigatorOptions = navigatorOptions; this.scrollbarOptions = scrollbarOptions; this.outlineHeight = height + scrollbarHeight; this.opposite = pick(navigatorOptions.opposite, Boolean(!navigatorEnabled && chart.inverted)); // #6262 var 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 and ordinal options breaks: baseXaxis.options.breaks, ordinal: baseXaxis.options.ordinal }, navigatorOptions.xAxis, { id: 'navigator-x-axis', yAxis: 'navigator-y-axis', isX: true, type: 'datetime', index: xAxisIndex, isInternal: true, offset: 0, keepOrdinalPadding: true, startOnTick: false, endOnTick: false, minPadding: 0, maxPadding: 0, zoomEnabled: false }, chart.inverted ? { offsets: [scrollbarHeight, 0, -scrollbarHeight, 0], width: height } : { offsets: [0, -scrollbarHeight, 0, scrollbarHeight], height: height })); navigator.yAxis = new Axis(chart, merge(navigatorOptions.yAxis, { id: 'navigator-y-axis', alignTicks: false, offset: 0, index: yAxisIndex, isInternal: true, zoomEnabled: false }, chart.inverted ? { width: height } : { height: height })); // 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: chart, navigatorAxis: { fake: true }, translate: function (value, reverse) { var axis = chart.xAxis[0], ext = axis.getExtremes(), scrollTrackWidth = axis.len - 2 * scrollbarHeight, 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 = (NavigatorAxis.AdditionsClass.prototype.toFixedRange.bind(navigator.xAxis.navigatorAxis)); } // Initialize the scrollbar if (chart.options.scrollbar.enabled) { chart.scrollbar = navigator.scrollbar = new Scrollbar(chart.renderer, merge(chart.options.scrollbar, { margin: navigator.navigatorEnabled ? 0 : 10, vertical: chart.inverted }), chart); addEvent(navigator.scrollbar, 'changed', function (e) { var range = navigator.size, to = range * this.to, from = range * this.from; navigator.hasDragged = navigator.scrollbar.hasDragged; navigator.render(0, 0, from, to); if (chart.options.scrollbar.liveRedraw || (e.DOMType !== 'mousemove' && e.DOMType !== 'touchmove')) { 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 * @param {boolean} [returnFalseOnNoBaseSeries] * as the param says. * @return {Highcharts.Dictionary<(number|undefined)>|undefined} */ Navigator.prototype.getUnionExtremes = function (returnFalseOnNoBaseSeries) { var baseAxis = this.chart.xAxis[0], navAxis = this.xAxis, navAxisOptions = navAxis.options, baseAxisOptions = baseAxis.options, 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. * @return {void} */ Navigator.prototype.setBaseSeries = function (baseSeriesOptions, redraw) { var 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, function (s) { return !s.options.isInternal; }).index : 0)); // Iterate through series and add the ones that should be shown in // navigator. (chart.series || []).forEach(function (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 * @param {boolean} addEvents * @param {boolean} [redraw] * @return {void} */ Navigator.prototype.updateNavigatorSeries = function (addEvents, redraw) { var navigator = this, chart = navigator.chart, baseSeries = navigator.baseSeries, baseOptions, mergedNavSeriesOptions, chartNavigatorSeriesOptions = navigator.navigatorOptions.series, baseNavigatorOptions, 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(function (navSeries) { var 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; }); // Go through each base series and merge the options to create new // series if (baseSeries && baseSeries.length) { baseSeries.forEach(function eachBaseSeries(base) { var 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 || {}; 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). var 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); 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(function (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 * @return {void} */ Navigator.prototype.addBaseSeriesEvents = function () { var 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) { addEvent(baseSeries[0].xAxis, 'foundExtremes', this.modifyBaseAxisExtremes); } baseSeries.forEach(function (base) { // Link base series show/hide to navigator series visibility addEvent(base, 'show', function () { if (this.navigatorSeries) { this.navigatorSeries.setVisible(true, false); } }); addEvent(base, 'hide', function () { if (this.navigatorSeries) { this.navigatorSeries.setVisible(false, false); } }); // Respond to updated data in the base series, unless explicitily // not adapting to data changes. if (this.navigatorOptions.adaptToUpdatedData !== false) { if (base.xAxis) { addEvent(base, 'updatedData', this.updatedDataHandler); } } // Handle series removal addEvent(base, 'remove', function () { if (this.navigatorSeries) { erase(navigator.series, this.navigatorSeries); if (defined(this.navigatorSeries.options)) { this.navigatorSeries.remove(false); } delete this.navigatorSeries; } }); }, this); }; /** * 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 */ Navigator.prototype.getBaseSeriesMin = function (currentSeriesMin) { return this.baseSeries.reduce(function (min, series) { // (#10193) return Math.min(min, series.xData ? 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 */ Navigator.prototype.modifyNavigatorAxisExtremes = function () { var xAxis = this.xAxis, unionExtremes; if (typeof xAxis.getExtremes !== 'undefined') { 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 */ Navigator.prototype.modifyBaseAxisExtremes = function () { var 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.options.overscroll, 0), newMax, newMin, 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'; 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 */ Navigator.prototype.updatedDataHandler = function () { var navigator = this.chart.navigator, baseSeries = this, navigatorSeries = this.navigatorSeries, xDataMin = navigator.getBaseSeriesMin(baseSeries.xData[0]); // If the scrollbar is scrolled all the way to the right, keep right as // new data comes in. navigator.stickToMax = navigator.reversedExtremes ? Math.round(navigator.zoomedMin) === 0 : Math.round(navigator.zoomedMax) >= Math.round(navigator.size); // Detect whether the zoomed area should stick to the minimum or // maximum. If the current axis minimum falls outside the new updated // dataset, we must adjust. navigator.stickToMin = isNumber(baseSeries.xAxis.min) && (baseSeries.xAxis.min <= xDataMin) && (!this.chart.fixedRange || !navigator.stickToMax); // 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 } }; /** * Add chart events, like redrawing navigator, when chart requires that. * * @private * @function Highcharts.Navigator#addChartEvents * @return {void} */ Navigator.prototype.addChartEvents = function () { 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 () { var 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 () { var chart = this, navigator = chart.navigator, marginName = navigator.opposite ? 'plotTop' : 'marginBottom'; if (chart.inverted) { marginName = navigator.opposite ? 'marginRight' : 'plotLeft'; } chart[marginName] = (chart[marginName] || 0) + (navigator.navigatorEnabled || !chart.inverted ? navigator.outlineHeight : 0) + navigator.navigatorOptions.margin; })); }; /** * Destroys allocated elements. * * @private * @function Highcharts.Navigator#destroy */ Navigator.prototype.destroy = function () { // 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(function (s) { if (s.destroy) { s.destroy(); } }); // Destroy properties [ 'series', 'xAxis', 'yAxis', 'shades', 'outline', 'scrollbarTrack', 'scrollbarRifles', 'scrollbarGroup', 'scrollbar', 'navigatorGroup', 'rendered' ].forEach(function (prop) { if (this[prop] && this[prop].destroy) { this[prop].destroy(); } this[prop] = null; }, this); // Destroy elements in collection [this.handles].forEach(function (coll) { destroyObjectProperties(coll); }, this); }; return Navigator; }()); // End of prototype if (!H.Navigator) { H.Navigator = Navigator; NavigatorAxis.compose(Axis); // 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) addEvent(Chart, 'beforeShowResetZoom', function () { var chartOptions = this.options, navigator = chartOptions.navigator, rangeSelector = chartOptions.rangeSelector; if (((navigator && navigator.enabled) || (rangeSelector && rangeSelector.enabled)) && ((!isTouchDevice && chartOptions.chart.zoomType === 'x') || (isTouchDevice && chartOptions.chart.pinchType === 'x'))) { return false; } }); // Initialize navigator for stock charts addEvent(Chart, 'beforeRender', function () { var options = this.options; if (options.navigator.enabled || options.scrollbar.enabled) { this.scroller = this.navigator = new Navigator(this); } }); // 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. addEvent(Chart, 'afterSetChartSize', function () { var legend = this.legend, navigator = this.navigator, scrollbarHeight, legendOptions, xAxis, yAxis; if (navigator) { legendOptions = legend && legend.options; xAxis = navigator.xAxis; yAxis = navigator.yAxis; scrollbarHeight = navigator.scrollbarHeight; // Compute the top position if (this.inverted) { navigator.left = navigator.opposite ? this.chartWidth - scrollbarHeight - navigator.height : this.spacing[3] + scrollbarHeight; navigator.top = this.plotTop + scrollbarHeight; } else { navigator.left = this.plotLeft + scrollbarHeight; navigator.top = navigator.navigatorOptions.top || this.chartHeight - navigator.height - scrollbarHeight - this.spacing[2] - (this.rangeSelector && this.extraBottomMargin ? this.rangeSelector.getHeight() : 0) - ((legendOptions && legendOptions.verticalAlign === 'bottom' && 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(); } } }); // Merge options, if no scrolling exists yet addEvent(Chart, 'update', function (e) { var 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; } }); // Initialize navigator, if no scrolling exists yet addEvent(Chart, 'afterUpdate', function (event) { if (!this.navigator && !this.scroller && (this.options.navigator.enabled || this.options.scrollbar.enabled)) { this.scroller = this.navigator = new Navigator(this); if (pick(event.redraw, true)) { this.redraw(event.animation); // #7067 } } }); // Handle adding new series addEvent(Chart, 'afterAddSeries', function () { if (this.navigator) { // Recompute which series should be shown in navigator, and add them this.navigator.setBaseSeries(null, false); } }); // Handle updating series addEvent(Series, 'afterUpdate', function () { if (this.chart.navigator && !this.options.isInternal) { this.chart.navigator.setBaseSeries(null, false); } }); Chart.prototype.callbacks.push(function (chart) { var extremes, navigator = chart.navigator; // Initialize the navigator if (navigator && chart.xAxis[0]) { extremes = chart.xAxis[0].getExtremes(); navigator.render(extremes.min, extremes.max); } }); } H.Navigator = Navigator; export default H.Navigator;