/** * (c) 2010-2019 Torstein Honsi * * License: www.highcharts.com/license */ 'use strict'; import H from './Globals.js'; import './Utilities.js'; import './Color.js'; import './Axis.js'; import './Chart.js'; import './Series.js'; import './Options.js'; import './Scrollbar.js'; var addEvent = H.addEvent, Axis = H.Axis, Chart = H.Chart, color = H.color, defaultDataGroupingUnits = H.defaultDataGroupingUnits, defaultOptions = H.defaultOptions, defined = H.defined, destroyObjectProperties = H.destroyObjectProperties, erase = H.erase, extend = H.extend, hasTouch = H.hasTouch, isArray = H.isArray, isNumber = H.isNumber, isTouchDevice = H.isTouchDevice, merge = H.merge, pick = H.pick, removeEvent = H.removeEvent, Scrollbar = H.Scrollbar, Series = H.Series, seriesTypes = H.seriesTypes, units = [].concat(defaultDataGroupingUnits), // copy 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 numbers = [].filter.call(arguments, isNumber); if (numbers.length) { return Math[extreme].apply(0, numbers); } }; // add more resolution to units units[4] = ['day', [1, 2, 3, 4]]; // allow more days units[5] = ['week', [1, 2, 3]]; // allow more weeks defaultSeriesType = 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 * @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 * @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 * @product highstock * @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 {*} * @default 0 * @product highstock * @apioption navigator.baseSeries */ /** * Enable or disable the navigator. * * @sample {highstock} stock/navigator/enabled/ * Disable the navigator * * @type {boolean} * @default true * @product highstock * @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 * @product highstock * @apioption navigator.opposite */ /** * The height of the navigator. * * @sample {highstock} stock/navigator/height/ * A higher navigator * * @product highstock */ 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 * * @product highstock */ 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 * @product highstock */ maskInside: true, /** * Options for the handles for dragging the zoomed area. * * @sample {highstock} stock/navigator/handles/ * Colored handles * * @product highstock */ handles: { /** * Width for handles. * * @sample {highstock} stock/navigator/styled-handles/ * Styled handles * * @since 6.0.0 * @product highstock */ width: 7, /** * Height for handles. * * @sample {highstock} stock/navigator/styled-handles/ * Styled handles * * @since 6.0.0 * @product highstock */ 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 * @product highstock */ symbols: ['navigator-handle', 'navigator-handle'], /** * Allows to enable/disable handles. * * @since 6.0.0 * @product highstock */ enabled: true, /** * The width for the handle border and the stripes inside. * * @sample {highstock} stock/navigator/styled-handles/ * Styled handles * * @since 6.0.0 * @product highstock * @apioption navigator.handles.lineWidth */ lineWidth: 1, /** * The fill for the handle. * * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject} * @product highstock */ backgroundColor: '#f2f2f2', /** * The stroke for the handle border and the stripes inside. * * @type {Highcharts.ColorString} * @product highstock */ 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) * @product highstock */ 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} * @default #cccccc * @product highstock */ 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} * @product highstock */ 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: * *
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 * * @product highstock */ series: { /** * The type of the navigator series. Defaults to `areaspline` if * defined, otherwise `line`. * * @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, /** * Data grouping options for the navigator series. * * @extends plotOptions.series.dataGrouping */ dataGrouping: { approximation: 'average', enabled: true, groupPixelWidth: 2, smoothed: true, units: units }, /** * 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', /** * Line color for the navigator series. Allows setting the color * while disallowing the default candlestick setting. * * @type {Highcharts.ColorString|null} */ lineColor: null, // #4602 marker: { enabled: false }, pointRange: 0, /** * 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: * *
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 * @product highstock */ 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 * @product highstock */ 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: * *
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 * @product highstock */ 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 } } }); /** * Draw one of the handles on the side of the zoomed range in the navigator * * @function Highcharts.Renderer#symbols.navigator-handle * * @param {boolean} inverted * flag for chart.inverted * * @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; 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 */ function Navigator(chart) { this.init(chart); } Navigator.prototype = { /** * 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} inverted * flag for chart.inverted * * @param {string} verb * use 'animate' or 'attr' */ 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} inverted * flag if chart is inverted * * @param {string} verb * use 'animate' or 'attr' */ 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, 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, // top edge 'L', left + outlineHeight, verticalMin, // top right of zoomed range 'L', left, verticalMin, // top left of z.r. 'L', left, zoomedMax, // bottom left of z.r. 'L', left + outlineHeight, zoomedMax, // bottom right of z.r. 'L', left + outlineHeight, navigatorTop + navigatorSize + scrollbarHeight // bottom edge ].concat(maskInside ? [ '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, // left 'L', zoomedMin, navigatorTop, // upper left of zoomed range 'L', zoomedMin, navigatorTop + outlineHeight, // lower left of z.r. 'L', zoomedMax, navigatorTop + outlineHeight, // lower right of z.r. 'L', zoomedMax, navigatorTop, // upper right of z.r. 'L', left + navigatorSize + scrollbarHeight * 2, navigatorTop // right ].concat(maskInside ? [ '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} inverted * flag if chart is inverted * * @param {string} verb * use 'animate' or 'attr' */ 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 */ 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 */ 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 */ render: function (min, max, pxMin, pxMax) { var navigator = this, chart = navigator.chart, navigatorWidth, scrollbarLeft, scrollbarTop, scrollbarHeight = navigator.scrollbarHeight, navigatorSize, xAxis = navigator.xAxis, scrollbarXAxis = xAxis.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; } // 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(H.correctFloat(newMax - newMin)); if (currentRange < minRange) { if (this.grabbedLeft) { pxMin = xAxis.toPixels(newMax - minRange, true); } else if (this.grabbedRight) { pxMax = xAxis.toPixels(newMin + minRange, true); } } else if (defined(maxRange) && currentRange > maxRange) { if (this.grabbedLeft) { pxMin = xAxis.toPixels(newMax - maxRange, true); } else if (this.grabbedRight) { pxMax = xAxis.toPixels(newMin + maxRange, true); } } // Handles are allowed to cross, but never exceed the plot area navigator.zoomedMax = Math.min(Math.max(pxMin, pxMax, 0), zoomedMax); navigator.zoomedMin = Math.min( Math.max( 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 */ 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(container, 'mousemove', mouseMoveHandler), addEvent(container.ownerDocument, 'mouseup', mouseUpHandler) ); // Touch events if (hasTouch) { eventsToUnbind.push( addEvent(container, '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: 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 {global.PointerEventObject} e * Mouse event * * @param {number} index * Index of a mask in Navigator.shades array */ 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.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 */ 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 */ 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 */ onMouseUp: function (e) { var navigator = this, chart = navigator.chart, xAxis = navigator.xAxis, scrollbar = navigator.scrollbar, unionExtremes, fixedMin, fixedMax, ext, DOMEvent = e.DOMEvent || e; 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.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') { navigator.grabbedLeft = navigator.grabbedRight = navigator.grabbedCenter = navigator.fixedWidth = navigator.fixedExtreme = navigator.otherHandlePos = navigator.hasDragged = navigator.dragOffset = null; } }, /** * Removes the event handlers attached previously with addEvents. * * @private * @function Highcharts.Navigator#removeEvents */ removeEvents: function () { if (this.eventsToUnbind) { this.eventsToUnbind.forEach(function (unbind) { unbind(); }); this.eventsToUnbind = undefined; } this.removeBaseSeriesEvents(); }, /** * Remove data events. * * @private * @function Highcharts.Navigator#removeBaseSeriesEvents */ 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 */ 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, !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, // #2436 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 = { 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); }, toFixedRange: Axis.prototype.toFixedRange, fake: true }; } // 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 {*} */ 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 {*} baseSeriesOptions * Additional series options for a navigator * * @param {boolean} [redraw] * Whether to redraw after update. */ 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) H.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.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 */ 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, // #6162 linkedTo: null, // #6734 group: 'nav', // for columns padXAxis: false, xAxis: 'navigator-x-axis', yAxis: 'navigator-y-axis', showInLegend: false, stacking: false, // #4823 isInternal: true }, // 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 ); // 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 = H.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 */ 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 * * @param {number} currentSeriesMin * Minium from the current series * * @return {number} Minimum from all series */ getBaseSeriesMin: function (currentSeriesMin) { return this.baseSeries.reduce( function (min, series) { return Math.min(min, series.xData[0]); }, 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: function () { var xAxis = this.xAxis, unionExtremes; if (xAxis.getExtremes) { 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: 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( 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: 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 */ 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 || navigator.scrollbar && this.xAxis[0] ); // #5709 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 */ 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); } }; H.Navigator = Navigator; /* * For Stock charts, override selection zooming with some special features * because X axis zooming is already allowed by the Navigator and Range * selector. */ addEvent(Axis, 'zoom', function (e) { var chart = this.chart, chartOptions = chart.options, zoomType = chartOptions.chart.zoomType, pinchType = chartOptions.chart.pinchType, previousZoom, navigator = chartOptions.navigator, rangeSelector = chartOptions.rangeSelector; if (this.isXAxis && ((navigator && navigator.enabled) || (rangeSelector && rangeSelector.enabled))) { // For y only zooming, ignore the X axis completely if (zoomType === 'y') { e.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 ( ( (!isTouchDevice && zoomType === 'xy') || (isTouchDevice && pinchType === 'xy') ) && this.options.range ) { previousZoom = this.previousZoom; if (defined(e.newMin)) { this.previousZoom = [this.min, this.max]; } else if (previousZoom) { e.newMin = previousZoom[0]; e.newMax = previousZoom[1]; delete this.previousZoom; } } } if (e.zoomed !== undefined) { e.preventDefault(); } }); /** * 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 ); } 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 () { if (!this.navigator && !this.scroller && (this.options.navigator.enabled || this.options.scrollbar.enabled) ) { this.scroller = this.navigator = new Navigator(this); } }); // 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); } });