UNPKG

highcharts

Version:
1,582 lines 61.3 kB
/* *
 *
 *  (c) 2010-2019 Torstein Honsi
 *
 *  License: www.highcharts.com/license
 *
 *  !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
 *
 * */
'use strict';
import H from './Globals.js';
/**
 * Define the time span for the button
 *
 * @typedef {"all"|"day"|"hour"|"millisecond"|"minute"|"month"|"second"|"week"|"year"|"ytd"} Highcharts.RangeSelectorButtonTypeValue
 */
/**
 * Callback function to react on button clicks.
 *
 * @callback Highcharts.RangeSelectorClickCallbackFunction
 *
 * @param {global.Event} e
 *        Event arguments.
 *
 * @param {boolean|undefined}
 *        Return false to cancel the default button event.
 */
/**
 * Callback function to parse values entered in the input boxes and return a
 * valid JavaScript time as milliseconds since 1970.
 *
 * @callback Highcharts.RangeSelectorParseCallbackFunction
 *
 * @param {string} value
 *        Input value to parse.
 *
 * @return {number}
 *         Parsed JavaScript time value.
 */
import U from './Utilities.js';
var defined = U.defined, destroyObjectProperties = U.destroyObjectProperties, discardElement = U.discardElement, extend = U.extend, isNumber = U.isNumber, objectEach = U.objectEach, pick = U.pick, pInt = U.pInt, splat = U.splat;
import './Axis.js';
import './Chart.js';
var addEvent = H.addEvent, Axis = H.Axis, Chart = H.Chart, css = H.css, createElement = H.createElement, defaultOptions = H.defaultOptions, fireEvent = H.fireEvent, merge = H.merge;
/* ************************************************************************** *
 * Start Range Selector code                                                  *
 * ************************************************************************** */
extend(defaultOptions, {
    /**
     * The range selector is a tool for selecting ranges to display within
     * the chart. It provides buttons to select preconfigured ranges in
     * the chart, like 1 day, 1 week, 1 month etc. It also provides input
     * boxes where min and max dates can be manually input.
     *
     * @product      highstock gantt
     * @optionparent rangeSelector
     */
    rangeSelector: {
        /**
         * Whether to enable all buttons from the start. By default buttons are
         * only enabled if the corresponding time range exists on the X axis,
         * but enabling all buttons allows for dynamically loading different
         * time ranges.
         *
         * @sample {highstock} stock/rangeselector/allbuttonsenabled-true/
         *         All buttons enabled
         *
         * @type      {boolean}
         * @default   false
         * @since     2.0.3
         * @apioption rangeSelector.allButtonsEnabled
         */
        /**
         * An array of configuration objects for the buttons.
         *
         * Defaults to:
         * ```js
         * buttons: [{
         *     type: 'month',
         *     count: 1,
         *     text: '1m'
         * }, {
         *     type: 'month',
         *     count: 3,
         *     text: '3m'
         * }, {
         *     type: 'month',
         *     count: 6,
         *     text: '6m'
         * }, {
         *     type: 'ytd',
         *     text: 'YTD'
         * }, {
         *     type: 'year',
         *     count: 1,
         *     text: '1y'
         * }, {
         *     type: 'all',
         *     text: 'All'
         * }]
         * ```
         *
         * @sample {highstock} stock/rangeselector/datagrouping/
         *         Data grouping by buttons
         *
         * @type      {Array<*>}
         * @apioption rangeSelector.buttons
         */
        /**
         * How many units of the defined type the button should span. If `type`
         * is "month" and `count` is 3, the button spans three months.
         *
         * @type      {number}
         * @default   1
         * @apioption rangeSelector.buttons.count
         */
        /**
         * Fires when clicking on the rangeSelector button. One parameter,
         * event, is passed to the function, containing common event
         * information.
         *
         * ```js
         * click: function(e) {
         *   console.log(this);
         * }
         * ```
         *
         * Return false to stop default button's click action.
         *
         * @sample {highstock} stock/rangeselector/button-click/
         *         Click event on the button
         *
         * @type      {Highcharts.RangeSelectorClickCallbackFunction}
         * @apioption rangeSelector.buttons.events.click
         */
        /**
         * Additional range (in milliseconds) added to the end of the calculated
         * time span.
         *
         * @sample {highstock} stock/rangeselector/min-max-offsets/
         *         Button offsets
         *
         * @type      {number}
         * @default   0
         * @since     6.0.0
         * @apioption rangeSelector.buttons.offsetMax
         */
        /**
         * Additional range (in milliseconds) added to the start of the
         * calculated time span.
         *
         * @sample {highstock} stock/rangeselector/min-max-offsets/
         *         Button offsets
         *
         * @type      {number}
         * @default   0
         * @since     6.0.0
         * @apioption rangeSelector.buttons.offsetMin
         */
        /**
         * When buttons apply dataGrouping on a series, by default zooming
         * in/out will deselect buttons and unset dataGrouping. Enable this
         * option to keep buttons selected when extremes change.
         *
         * @sample {highstock} stock/rangeselector/preserve-datagrouping/
         *         Different preserveDataGrouping settings
         *
         * @type      {boolean}
         * @default   false
         * @since     6.1.2
         * @apioption rangeSelector.buttons.preserveDataGrouping
         */
        /**
         * A custom data grouping object for each button.
         *
         * @see [series.dataGrouping](#plotOptions.series.dataGrouping)
         *
         * @sample {highstock} stock/rangeselector/datagrouping/
         *         Data grouping by range selector buttons
         *
         * @type      {*}
         * @extends   plotOptions.series.dataGrouping
         * @apioption rangeSelector.buttons.dataGrouping
         */
        /**
         * The text for the button itself.
         *
         * @type      {string}
         * @apioption rangeSelector.buttons.text
         */
        /**
         * Defined the time span for the button. Can be one of `millisecond`,
         * `second`, `minute`, `hour`, `day`, `week`, `month`, `year`, `ytd`,
         * and `all`.
         *
         * @type       {Highcharts.RangeSelectorButtonTypeValue}
         * @apioption  rangeSelector.buttons.type
         */
        /**
         * The space in pixels between the buttons in the range selector.
         *
         * @type      {number}
         * @default   0
         * @apioption rangeSelector.buttonSpacing
         */
        /**
         * Enable or disable the range selector.
         *
         * @sample {highstock} stock/rangeselector/enabled/
         *         Disable the range selector
         *
         * @type      {boolean}
         * @default   true
         * @apioption rangeSelector.enabled
         */
        /**
         * The vertical alignment of the rangeselector box. Allowed properties
         * are `top`, `middle`, `bottom`.
         *
         * @sample {highstock} stock/rangeselector/vertical-align-middle/
         *         Middle
         * @sample {highstock} stock/rangeselector/vertical-align-bottom/
         *         Bottom
         *
         * @type  {Highcharts.VerticalAlignValue}
         * @since 6.0.0
         */
        verticalAlign: 'top',
        /**
         * A collection of attributes for the buttons. The object takes SVG
         * attributes like `fill`, `stroke`, `stroke-width`, as well as `style`,
         * a collection of CSS properties for the text.
         *
         * The object can also be extended with states, so you can set
         * presentational options for `hover`, `select` or `disabled` button
         * states.
         *
         * CSS styles for the text label.
         *
         * In styled mode, the buttons are styled by the
         * `.highcharts-range-selector-buttons .highcharts-button` rule with its
         * different states.
         *
         * @sample {highstock} stock/rangeselector/styling/
         *         Styling the buttons and inputs
         *
         * @type {Highcharts.SVGAttributes}
         */
        buttonTheme: {
            /** @ignore */
            width: 28,
            /** @ignore */
            height: 18,
            /** @ignore */
            padding: 2,
            /** @ignore */
            zIndex: 7 // #484, #852
        },
        /**
         * When the rangeselector is floating, the plot area does not reserve
         * space for it. This opens for positioning anywhere on the chart.
         *
         * @sample {highstock} stock/rangeselector/floating/
         *         Placing the range selector between the plot area and the
         *         navigator
         *
         * @since 6.0.0
         */
        floating: false,
        /**
         * The x offset of the range selector relative to its horizontal
         * alignment within `chart.spacingLeft` and `chart.spacingRight`.
         *
         * @since 6.0.0
         */
        x: 0,
        /**
         * The y offset of the range selector relative to its horizontal
         * alignment within `chart.spacingLeft` and `chart.spacingRight`.
         *
         * @since 6.0.0
         */
        y: 0,
        /**
         * Deprecated. The height of the range selector. Currently it is
         * calculated dynamically.
         *
         * @deprecated
         * @type  {number|undefined}
         * @since 2.1.9
         */
        height: void 0,
        /**
         * The border color of the date input boxes.
         *
         * @sample {highstock} stock/rangeselector/styling/
         *         Styling the buttons and inputs
         *
         * @type      {Highcharts.ColorString}
         * @default   #cccccc
         * @since     1.3.7
         * @apioption rangeSelector.inputBoxBorderColor
         */
        /**
         * The pixel height of the date input boxes.
         *
         * @sample {highstock} stock/rangeselector/styling/
         *         Styling the buttons and inputs
         *
         * @type      {number}
         * @default   17
         * @since     1.3.7
         * @apioption rangeSelector.inputBoxHeight
         */
        /**
         * CSS for the container DIV holding the input boxes. Deprecated as
         * of 1.2.5\. Use [inputPosition](#rangeSelector.inputPosition) instead.
         *
         * @sample {highstock} stock/rangeselector/styling/
         *         Styling the buttons and inputs
         *
         * @deprecated
         * @type      {Highcharts.CSSObject}
         * @apioption rangeSelector.inputBoxStyle
         */
        /**
         * The pixel width of the date input boxes.
         *
         * @sample {highstock} stock/rangeselector/styling/
         *         Styling the buttons and inputs
         *
         * @type      {number}
         * @default   90
         * @since     1.3.7
         * @apioption rangeSelector.inputBoxWidth
         */
        /**
         * The date format in the input boxes when not selected for editing.
         * Defaults to `%b %e, %Y`.
         *
         * @sample {highstock} stock/rangeselector/input-format/
         *         Milliseconds in the range selector
         *
         * @type      {string}
         * @default   %b %e, %Y
         * @apioption rangeSelector.inputDateFormat
         */
        /**
         * A custom callback function to parse values entered in the input boxes
         * and return a valid JavaScript time as milliseconds since 1970.
         *
         * @sample {highstock} stock/rangeselector/input-format/
         *         Milliseconds in the range selector
         *
         * @type      {Highcharts.RangeSelectorParseCallbackFunction}
         * @since     1.3.3
         * @apioption rangeSelector.inputDateParser
         */
        /**
         * The date format in the input boxes when they are selected for
         * editing. This must be a format that is recognized by JavaScript
         * Date.parse.
         *
         * @sample {highstock} stock/rangeselector/input-format/
         *         Milliseconds in the range selector
         *
         * @type      {string}
         * @default   %Y-%m-%d
         * @apioption rangeSelector.inputEditDateFormat
         */
        /**
         * Enable or disable the date input boxes. Defaults to enabled when
         * there is enough space, disabled if not (typically mobile).
         *
         * @sample {highstock} stock/rangeselector/input-datepicker/
         *         Extending the input with a jQuery UI datepicker
         *
         * @type      {boolean}
         * @default   true
         * @apioption rangeSelector.inputEnabled
         */
        /**
         * Positioning for the input boxes. Allowed properties are `align`,
         *  `x` and `y`.
         *
         * @since 1.2.4
         */
        inputPosition: {
            /**
             * The alignment of the input box. Allowed properties are `left`,
             * `center`, `right`.
             *
             * @sample {highstock} stock/rangeselector/input-button-position/
             *         Alignment
             *
             * @type  {Highcharts.AlignValue}
             * @since 6.0.0
             */
            align: 'right',
            /**
             * X offset of the input row.
             */
            x: 0,
            /**
             * Y offset of the input row.
             */
            y: 0
        },
        /**
         * The index of the button to appear pre-selected.
         *
         * @type      {number}
         * @apioption rangeSelector.selected
         */
        /**
         * Positioning for the button row.
         *
         * @since 1.2.4
         */
        buttonPosition: {
            /**
             * The alignment of the input box. Allowed properties are `left`,
             * `center`, `right`.
             *
             * @sample {highstock} stock/rangeselector/input-button-position/
             *         Alignment
             *
             * @type  {Highcharts.AlignValue}
             * @since 6.0.0
             */
            align: 'left',
            /**
             * X offset of the button row.
             */
            x: 0,
            /**
             * Y offset of the button row.
             */
            y: 0
        },
        /**
         * CSS for the HTML inputs in the range selector.
         *
         * In styled mode, the inputs are styled by the
         * `.highcharts-range-input text` rule in SVG mode, and
         * `input.highcharts-range-selector` when active.
         *
         * @sample {highstock} stock/rangeselector/styling/
         *         Styling the buttons and inputs
         *
         * @type      {Highcharts.CSSObject}
         * @apioption rangeSelector.inputStyle
         */
        /**
         * CSS styles for the labels - the Zoom, From and To texts.
         *
         * In styled mode, the labels are styled by the
         * `.highcharts-range-label` class.
         *
         * @sample {highstock} stock/rangeselector/styling/
         *         Styling the buttons and inputs
         *
         * @type {Highcharts.CSSObject}
         */
        labelStyle: {
            /** @ignore */
            color: '#666666'
        }
    }
});
defaultOptions.lang = merge(defaultOptions.lang, 
/**
 * Language object. The language object is global and it can't be set
 * on each chart initialization. Instead, use `Highcharts.setOptions` to
 * set it before any chart is initialized.
 *
 * ```js
 * Highcharts.setOptions({
 *     lang: {
 *         months: [
 *             'Janvier', 'Février', 'Mars', 'Avril',
 *             'Mai', 'Juin', 'Juillet', 'Août',
 *             'Septembre', 'Octobre', 'Novembre', 'Décembre'
 *         ],
 *         weekdays: [
 *             'Dimanche', 'Lundi', 'Mardi', 'Mercredi',
 *             'Jeudi', 'Vendredi', 'Samedi'
 *         ]
 *     }
 * });
 * ```
 *
 * @optionparent lang
 */
{
    /**
     * The text for the label for the range selector buttons.
     *
     * @product highstock gantt
     */
    rangeSelectorZoom: 'Zoom',
    /**
     * The text for the label for the "from" input box in the range
     * selector.
     *
     * @product highstock gantt
     */
    rangeSelectorFrom: 'From',
    /**
     * The text for the label for the "to" input box in the range selector.
     *
     * @product highstock gantt
     */
    rangeSelectorTo: 'To'
});
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
 * The range selector.
 *
 * @private
 * @class
 * @name Highcharts.RangeSelector
 * @param {Highcharts.Chart} chart
 */
function RangeSelector(chart) {
    // Run RangeSelector
    this.init(chart);
}
RangeSelector.prototype = {
    /**
     * The method to run when one of the buttons in the range selectors is
     * clicked
     *
     * @private
     * @function Highcharts.RangeSelector#clickButton
     * @param {number} i
     *        The index of the button
     * @param {boolean} [redraw]
     * @return {void}
     */
    clickButton: function (i, redraw) {
        var rangeSelector = this, chart = rangeSelector.chart, rangeOptions = rangeSelector.buttonOptions[i], baseAxis = chart.xAxis[0], unionExtremes = (chart.scroller && chart.scroller.getUnionExtremes()) || baseAxis || {}, dataMin = unionExtremes.dataMin, dataMax = unionExtremes.dataMax, newMin, newMax = baseAxis && Math.round(Math.min(baseAxis.max, pick(dataMax, baseAxis.max))), // #1568
        type = rangeOptions.type, baseXAxisOptions, range = rangeOptions._range, rangeMin, minSetting, rangeSetting, ctx, ytdExtremes, dataGrouping = rangeOptions.dataGrouping;
        // chart has no data, base series is removed
        if (dataMin === null || dataMax === null) {
            return;
        }
        // Set the fixed range before range is altered
        chart.fixedRange = range;
        // Apply dataGrouping associated to button
        if (dataGrouping) {
            this.forcedDataGrouping = true;
            Axis.prototype.setDataGrouping.call(baseAxis || { chart: this.chart }, dataGrouping, false);
            this.frozenStates = rangeOptions.preserveDataGrouping;
        }
        // Apply range
        if (type === 'month' || type === 'year') {
            if (!baseAxis) {
                // This is set to the user options and picked up later when the
                // axis is instantiated so that we know the min and max.
                range = rangeOptions;
            }
            else {
                ctx = {
                    range: rangeOptions,
                    max: newMax,
                    chart: chart,
                    dataMin: dataMin,
                    dataMax: dataMax
                };
                newMin = baseAxis.minFromRange.call(ctx);
                if (isNumber(ctx.newMax)) {
                    newMax = ctx.newMax;
                }
            }
            // Fixed times like minutes, hours, days
        }
        else if (range) {
            newMin = Math.max(newMax - range, dataMin);
            newMax = Math.min(newMin + range, dataMax);
        }
        else if (type === 'ytd') {
            // On user clicks on the buttons, or a delayed action running from
            // the beforeRender event (below), the baseAxis is defined.
            if (baseAxis) {
                // When "ytd" is the pre-selected button for the initial view,
                // its calculation is delayed and rerun in the beforeRender
                // event (below). When the series are initialized, but before
                // the chart is rendered, we have access to the xData array
                // (#942).
                if (typeof dataMax === 'undefined') {
                    dataMin = Number.MAX_VALUE;
                    dataMax = Number.MIN_VALUE;
                    chart.series.forEach(function (series) {
                        // reassign it to the last item
                        var xData = series.xData;
                        dataMin = Math.min(xData[0], dataMin);
                        dataMax = Math.max(xData[xData.length - 1], dataMax);
                    });
                    redraw = false;
                }
                ytdExtremes = rangeSelector.getYTDExtremes(dataMax, dataMin, chart.time.useUTC);
                newMin = rangeMin = ytdExtremes.min;
                newMax = ytdExtremes.max;
                // "ytd" is pre-selected. We don't yet have access to processed
                // point and extremes data (things like pointStart and pointInterval
                // are missing), so we delay the process (#942)
            }
            else {
                rangeSelector.deferredYTDClick = i;
                return;
            }
        }
        else if (type === 'all' && baseAxis) {
            newMin = dataMin;
            newMax = dataMax;
        }
        newMin += rangeOptions._offsetMin;
        newMax += rangeOptions._offsetMax;
        rangeSelector.setSelected(i);
        // Update the chart
        if (!baseAxis) {
            // Axis not yet instanciated. Temporarily set min and range
            // options and remove them on chart load (#4317).
            baseXAxisOptions = splat(chart.options.xAxis)[0];
            rangeSetting = baseXAxisOptions.range;
            baseXAxisOptions.range = range;
            minSetting = baseXAxisOptions.min;
            baseXAxisOptions.min = rangeMin;
            addEvent(chart, 'load', function resetMinAndRange() {
                baseXAxisOptions.range = rangeSetting;
                baseXAxisOptions.min = minSetting;
            });
        }
        else {
            // Existing axis object. Set extremes after render time.
            baseAxis.setExtremes(newMin, newMax, pick(redraw, 1), null, // auto animation
            {
                trigger: 'rangeSelectorButton',
                rangeSelectorButton: rangeOptions
            });
        }
    },
    /**
     * Set the selected option. This method only sets the internal flag, it
     * doesn't update the buttons or the actual zoomed range.
     *
     * @private
     * @function Highcharts.RangeSelector#setSelected
     * @param {number} [selected]
     * @return {void}
     */
    setSelected: function (selected) {
        this.selected = this.options.selected = selected;
    },
    /**
     * The default buttons for pre-selecting time frames
     */
    defaultButtons: [{
            type: 'month',
            count: 1,
            text: '1m'
        }, {
            type: 'month',
            count: 3,
            text: '3m'
        }, {
            type: 'month',
            count: 6,
            text: '6m'
        }, {
            type: 'ytd',
            text: 'YTD'
        }, {
            type: 'year',
            count: 1,
            text: '1y'
        }, {
            type: 'all',
            text: 'All'
        }],
    /**
     * Initialize the range selector
     *
     * @private
     * @function Highcharts.RangeSelector#init
     * @param {Highcharts.Chart} chart
     * @return {void}
     */
    init: function (chart) {
        var rangeSelector = this, options = chart.options.rangeSelector, buttonOptions = options.buttons ||
            [].concat(rangeSelector.defaultButtons), selectedOption = options.selected, blurInputs = function () {
            var minInput = rangeSelector.minInput, maxInput = rangeSelector.maxInput;
            // #3274 in some case blur is not defined
            if (minInput && minInput.blur) {
                fireEvent(minInput, 'blur');
            }
            if (maxInput && maxInput.blur) {
                fireEvent(maxInput, 'blur');
            }
        };
        rangeSelector.chart = chart;
        rangeSelector.options = options;
        rangeSelector.buttons = [];
        rangeSelector.buttonOptions = buttonOptions;
        this.unMouseDown = addEvent(chart.container, 'mousedown', blurInputs);
        this.unResize = addEvent(chart, 'resize', blurInputs);
        // Extend the buttonOptions with actual range
        buttonOptions.forEach(rangeSelector.computeButtonRange);
        // zoomed range based on a pre-selected button index
        if (typeof selectedOption !== 'undefined' &&
            buttonOptions[selectedOption]) {
            this.clickButton(selectedOption, false);
        }
        addEvent(chart, 'load', function () {
            // If a data grouping is applied to the current button, release it
            // when extremes change
            if (chart.xAxis && chart.xAxis[0]) {
                addEvent(chart.xAxis[0], 'setExtremes', function (e) {
                    if (this.max - this.min !==
                        chart.fixedRange &&
                        e.trigger !== 'rangeSelectorButton' &&
                        e.trigger !== 'updatedData' &&
                        rangeSelector.forcedDataGrouping &&
                        !rangeSelector.frozenStates) {
                        this.setDataGrouping(false, false);
                    }
                });
            }
        });
    },
    /**
     * Dynamically update the range selector buttons after a new range has been
     * set
     *
     * @private
     * @function Highcharts.RangeSelector#updateButtonStates
     * @return {void}
     */
    updateButtonStates: function () {
        var rangeSelector = this, chart = this.chart, baseAxis = chart.xAxis[0], actualRange = Math.round(baseAxis.max - baseAxis.min), hasNoData = !baseAxis.hasVisibleSeries, day = 24 * 36e5, // A single day in milliseconds
        unionExtremes = (chart.scroller &&
            chart.scroller.getUnionExtremes()) || baseAxis, dataMin = unionExtremes.dataMin, dataMax = unionExtremes.dataMax, ytdExtremes = rangeSelector.getYTDExtremes(dataMax, dataMin, chart.time.useUTC), ytdMin = ytdExtremes.min, ytdMax = ytdExtremes.max, selected = rangeSelector.selected, selectedExists = isNumber(selected), allButtonsEnabled = rangeSelector.options.allButtonsEnabled, buttons = rangeSelector.buttons;
        rangeSelector.buttonOptions.forEach(function (rangeOptions, i) {
            var range = rangeOptions._range, type = rangeOptions.type, count = rangeOptions.count || 1, button = buttons[i], state = 0, disable, select, offsetRange = rangeOptions._offsetMax -
                rangeOptions._offsetMin, isSelected = i === selected, 
            // Disable buttons where the range exceeds what is allowed in
            // the current view
            isTooGreatRange = range >
                dataMax - dataMin, 
            // Disable buttons where the range is smaller than the minimum
            // range
            isTooSmallRange = range < baseAxis.minRange, 
            // Do not select the YTD button if not explicitly told so
            isYTDButNotSelected = false, 
            // Disable the All button if we're already showing all
            isAllButAlreadyShowingAll = false, isSameRange = range === actualRange;
            // Months and years have a variable range so we check the extremes
            if ((type === 'month' || type === 'year') &&
                (actualRange + 36e5 >=
                    { month: 28, year: 365 }[type] * day * count - offsetRange) &&
                (actualRange - 36e5 <=
                    { month: 31, year: 366 }[type] * day * count + offsetRange)) {
                isSameRange = true;
            }
            else if (type === 'ytd') {
                isSameRange = (ytdMax - ytdMin + offsetRange) === actualRange;
                isYTDButNotSelected = !isSelected;
            }
            else if (type === 'all') {
                isSameRange = (baseAxis.max - baseAxis.min >=
                    dataMax - dataMin);
                isAllButAlreadyShowingAll = (!isSelected &&
                    selectedExists &&
                    isSameRange);
            }
            // The new zoom area happens to match the range for a button - mark
            // it selected. This happens when scrolling across an ordinal gap.
            // It can be seen in the intraday demos when selecting 1h and scroll
            // across the night gap.
            disable = (!allButtonsEnabled &&
                (isTooGreatRange ||
                    isTooSmallRange ||
                    isAllButAlreadyShowingAll ||
                    hasNoData));
            select = ((isSelected && isSameRange) ||
                (isSameRange && !selectedExists && !isYTDButNotSelected) ||
                (isSelected && rangeSelector.frozenStates));
            if (disable) {
                state = 3;
            }
            else if (select) {
                selectedExists = true; // Only one button can be selected
                state = 2;
            }
            // If state has changed, update the button
            if (button.state !== state) {
                button.setState(state);
                // Reset (#9209)
                if (state === 0 && selected === i) {
                    rangeSelector.setSelected(null);
                }
            }
        });
    },
    /**
     * Compute and cache the range for an individual button
     *
     * @private
     * @function Highcharts.RangeSelector#computeButtonRange
     * @param {Highcharts.RangeSelectorButtonsOptions} rangeOptions
     * @return {void}
     */
    computeButtonRange: function (rangeOptions) {
        var type = rangeOptions.type, count = rangeOptions.count || 1, 
        // these time intervals have a fixed number of milliseconds, as
        // opposed to month, ytd and year
        fixedTimes = {
            millisecond: 1,
            second: 1000,
            minute: 60 * 1000,
            hour: 3600 * 1000,
            day: 24 * 3600 * 1000,
            week: 7 * 24 * 3600 * 1000
        };
        // Store the range on the button object
        if (fixedTimes[type]) {
            rangeOptions._range = fixedTimes[type] * count;
        }
        else if (type === 'month' || type === 'year') {
            rangeOptions._range = {
                month: 30,
                year: 365
            }[type] * 24 * 36e5 * count;
        }
        rangeOptions._offsetMin = pick(rangeOptions.offsetMin, 0);
        rangeOptions._offsetMax = pick(rangeOptions.offsetMax, 0);
        rangeOptions._range +=
            rangeOptions._offsetMax - rangeOptions._offsetMin;
    },
    /**
     * Set the internal and displayed value of a HTML input for the dates
     *
     * @private
     * @function Highcharts.RangeSelector#setInputValue
     * @param {string} name
     * @param {number} [inputTime]
     * @return {void}
     */
    setInputValue: function (name, inputTime) {
        var options = this.chart.options.rangeSelector, time = this.chart.time, input = this[name + 'Input'];
        if (defined(inputTime)) {
            input.previousValue = input.HCTime;
            input.HCTime = inputTime;
        }
        input.value = time.dateFormat(options.inputEditDateFormat || '%Y-%m-%d', input.HCTime);
        this[name + 'DateBox'].attr({
            text: time.dateFormat(options.inputDateFormat || '%b %e, %Y', input.HCTime)
        });
    },
    /**
     * @private
     * @function Highcharts.RangeSelector#showInput
     * @param {string} name
     * @return {void}
     */
    showInput: function (name) {
        var inputGroup = this.inputGroup, dateBox = this[name + 'DateBox'];
        css(this[name + 'Input'], {
            left: (inputGroup.translateX + dateBox.x) + 'px',
            top: inputGroup.translateY + 'px',
            width: (dateBox.width - 2) + 'px',
            height: (dateBox.height - 2) + 'px',
            border: '2px solid silver'
        });
    },
    /**
     * @private
     * @function Highcharts.RangeSelector#hideInput
     * @param {string} name
     * @return {void}
     */
    hideInput: function (name) {
        css(this[name + 'Input'], {
            border: 0,
            width: '1px',
            height: '1px'
        });
        this.setInputValue(name);
    },
    /**
     * Draw either the 'from' or the 'to' HTML input box of the range selector
     *
     * @private
     * @function Highcharts.RangeSelector#drawInput
     * @param {string} name
     * @return {void}
     */
    drawInput: function (name) {
        var rangeSelector = this, chart = rangeSelector.chart, chartStyle = chart.renderer.style || {}, renderer = chart.renderer, options = chart.options.rangeSelector, lang = defaultOptions.lang, div = rangeSelector.div, isMin = name === 'min', input, label, dateBox, inputGroup = this.inputGroup;
        /**
         * @private
         */
        function updateExtremes() {
            var inputValue = input.value, value = (options.inputDateParser || Date.parse)(inputValue), chartAxis = chart.xAxis[0], dataAxis = chart.scroller && chart.scroller.xAxis ?
                chart.scroller.xAxis :
                chartAxis, dataMin = dataAxis.dataMin, dataMax = dataAxis.dataMax;
            if (value !== input.previousValue) {
                input.previousValue = value;
                // If the value isn't parsed directly to a value by the
                // browser's Date.parse method, like YYYY-MM-DD in IE, try
                // parsing it a different way
                if (!isNumber(value)) {
                    value = inputValue.split('-');
                    value = Date.UTC(pInt(value[0]), pInt(value[1]) - 1, pInt(value[2]));
                }
                if (isNumber(value)) {
                    // Correct for timezone offset (#433)
                    if (!chart.time.useUTC) {
                        value =
                            value + new Date().getTimezoneOffset() * 60 * 1000;
                    }
                    // Validate the extremes. If it goes beyound the data min or
                    // max, use the actual data extreme (#2438).
                    if (isMin) {
                        if (value > rangeSelector.maxInput.HCTime) {
                            value = void 0;
                        }
                        else if (value < dataMin) {
                            value = dataMin;
                        }
                    }
                    else {
                        if (value < rangeSelector.minInput.HCTime) {
                            value = void 0;
                        }
                        else if (value > dataMax) {
                            value = dataMax;
                        }
                    }
                    // Set the extremes
                    if (typeof value !== 'undefined') { // @todo typof undefined
                        chartAxis.setExtremes(isMin ? value : chartAxis.min, isMin ? chartAxis.max : value, void 0, void 0, { trigger: 'rangeSelectorInput' });
                    }
                }
            }
        }
        // Create the text label
        this[name + 'Label'] = label = renderer
            .label(lang[isMin ? 'rangeSelectorFrom' : 'rangeSelectorTo'], this.inputGroup.offset)
            .addClass('highcharts-range-label')
            .attr({
            padding: 2
        })
            .add(inputGroup);
        inputGroup.offset += label.width + 5;
        // Create an SVG label that shows updated date ranges and and records
        // click events that bring in the HTML input.
        this[name + 'DateBox'] = dateBox = renderer
            .label('', inputGroup.offset)
            .addClass('highcharts-range-input')
            .attr({
            padding: 2,
            width: options.inputBoxWidth || 90,
            height: options.inputBoxHeight || 17,
            'text-align': 'center'
        })
            .on('click', function () {
            // If it is already focused, the onfocus event doesn't fire
            // (#3713)
            rangeSelector.showInput(name);
            rangeSelector[name + 'Input'].focus();
        });
        if (!chart.styledMode) {
            dateBox.attr({
                stroke: options.inputBoxBorderColor || '#cccccc',
                'stroke-width': 1
            });
        }
        dateBox.add(inputGroup);
        inputGroup.offset += dateBox.width + (isMin ? 10 : 0);
        // Create the HTML input element. This is rendered as 1x1 pixel then set
        // to the right size when focused.
        this[name + 'Input'] = input = createElement('input', {
            name: name,
            className: 'highcharts-range-selector',
            type: 'text'
        }, {
            top: chart.plotTop + 'px' // prevent jump on focus in Firefox
        }, div);
        if (!chart.styledMode) {
            // Styles
            label.css(merge(chartStyle, options.labelStyle));
            dateBox.css(merge({
                color: '#333333'
            }, chartStyle, options.inputStyle));
            css(input, extend({
                position: 'absolute',
                border: 0,
                width: '1px',
                height: '1px',
                padding: 0,
                textAlign: 'center',
                fontSize: chartStyle.fontSize,
                fontFamily: chartStyle.fontFamily,
                top: '-9999em' // #4798
            }, options.inputStyle));
        }
        // Blow up the input box
        input.onfocus = function () {
            rangeSelector.showInput(name);
        };
        // Hide away the input box
        input.onblur = function () {
            // update extermes only when inputs are active
            if (input === H.doc.activeElement) { // Only when focused
                // Update also when no `change` event is triggered, like when
                // clicking inside the SVG (#4710)
                updateExtremes();
            }
            // #10404 - move hide and blur outside focus
            rangeSelector.hideInput(name);
            input.blur(); // #4606
        };
        // handle changes in the input boxes
        input.onchange = updateExtremes;
        input.onkeypress = function (event) {
            // IE does not fire onchange on enter
            if (event.keyCode === 13) {
                updateExtremes();
            }
        };
    },
    /**
     * Get the position of the range selector buttons and inputs. This can be
     * overridden from outside for custom positioning.
     *
     * @private
     * @function Highcharts.RangeSelector#getPosition
     *
     * @return {Highcharts.Dictionary<number>}
     */
    getPosition: function () {
        var chart = this.chart, options = chart.options.rangeSelector, top = options.verticalAlign === 'top' ?
            chart.plotTop - chart.axisOffset[0] :
            0; // set offset only for varticalAlign top
        return {
            buttonTop: top + options.buttonPosition.y,
            inputTop: top + options.inputPosition.y - 10
        };
    },
    /**
     * Get the extremes of YTD. Will choose dataMax if its value is lower than
     * the current timestamp. Will choose dataMin if its value is higher than
     * the timestamp for the start of current year.
     *
     * @private
     * @function Highcharts.RangeSelector#getYTDExtremes
     *
     * @param {number} dataMax
     *
     * @param {number} dataMin
     *
     * @return {*}
     *         Returns min and max for the YTD
     */
    getYTDExtremes: function (dataMax, dataMin, useUTC) {
        var time = this.chart.time, min, now = new time.Date(dataMax), year = time.get('FullYear', now), startOfYear = useUTC ?
            time.Date.UTC(year, 0, 1) : // eslint-disable-line new-cap
            +new time.Date(year, 0, 1);
        min = Math.max(dataMin || 0, startOfYear);
        now = now.getTime();
        return {
            max: Math.min(dataMax || now, now),
            min: min
        };
    },
    /**
     * Render the range selector including the buttons and the inputs. The first
     * time render is called, the elements are created and positioned. On
     * subsequent calls, they are moved and updated.
     *
     * @private
     * @function Highcharts.RangeSelector#render
     * @param {number} [min]
     *        X axis minimum
     * @param {number} [max]
     *        X axis maximum
     * @return {void}
     */
    render: function (min, max) {
        var rangeSelector = this, chart = rangeSelector.chart, renderer = chart.renderer, container = chart.container, chartOptions = chart.options, navButtonOptions = (chartOptions.exporting &&
            chartOptions.exporting.enabled !== false &&
            chartOptions.navigation &&
            chartOptions.navigation.buttonOptions), lang = defaultOptions.lang, div = rangeSelector.div, options = chartOptions.rangeSelector, 
        // Place inputs above the container
        inputsZIndex = pick(chartOptions.chart.style &&
            chartOptions.chart.style.zIndex, 0) + 1, floating = options.floating, buttons = rangeSelector.buttons, inputGroup = rangeSelector.inputGroup, buttonTheme = options.buttonTheme, buttonPosition = options.buttonPosition, inputPosition = options.inputPosition, inputEnabled = options.inputEnabled, states = buttonTheme && buttonTheme.states, plotLeft = chart.plotLeft, buttonLeft, buttonGroup = rangeSelector.buttonGroup, group, groupHeight, rendered = rangeSelector.rendered, verticalAlign = rangeSelector.options.verticalAlign, legend = chart.legend, legendOptions = legend && legend.options, buttonPositionY = buttonPosition.y, inputPositionY = inputPosition.y, animate = rendered || false, verb = animate ? 'animate' : 'attr', exportingX = 0, alignTranslateY, legendHeight, minPosition, translateY = 0, translateX;
        if (options.enabled === false) {
            return;
        }
        // create the elements
        if (!rendered) {
            rangeSelector.group = group = renderer.g('range-selector-group')
                .attr({
                zIndex: 7
            })
                .add();
            rangeSelector.buttonGroup = buttonGroup =
                renderer.g('range-selector-buttons').add(group);
            rangeSelector.zoomText = renderer
                .text(lang.rangeSelectorZoom, 0, 15)
                .add(buttonGroup);
            if (!chart.styledMode) {
                rangeSelector.zoomText.css(options.labelStyle);
                buttonTheme['stroke-width'] =
                    pick(buttonTheme['stroke-width'], 0);
            }
            rangeSelector.buttonOptions.forEach(function (rangeOptions, i) {
                buttons[i] = renderer
                    .button(rangeOptions.text, 0, 0, function (e) {
                    // extract events from button object and call
                    var buttonEvents = (rangeOptions.events &&
                        rangeOptions.events.click), callDefaultEvent;
                    if (buttonEvents) {
                        callDefaultEvent =
                            buttonEvents.call(rangeOptions, e);
                    }
                    if (callDefaultEvent !== false) {
                        rangeSelector.clickButton(i);
                    }
                    rangeSelector.isActive = true;
                }, buttonTheme, states && states.hover, states && states.select, states && states.disabled)
                    .attr({
                    'text-align': 'center'
                })
                    .add(buttonGroup);
            });
            // first create a wrapper outside the container in order to make
            // the inputs work and make export correct
            if (inputEnabled !== false) {
                rangeSelector.div = div = createElement('div', null, {
                    position: 'relative',
                    height: 0,
                    zIndex: inputsZIndex
                });
                container.parentNode.insertBefore(div, container);
                // Create the group to keep the inputs
                rangeSelector.inputGroup = inputGroup =
                    renderer.g('input-group').add(group);
                inputGroup.offset = 0;
                rangeSelector.drawInput('min');
                rangeSelector.drawInput('max');
            }
        }
        // #8769, allow dynamically updating margins
        rangeSelector.zoomText[verb]({
            x: pick(plotLeft + buttonPosition.x, plotLeft)
        });
        // button start position
        buttonLeft = pick(plotLeft + buttonPosition.x, plotLeft) +
            rangeSelector.zoomText.getBBox().width + 5;
        rangeSelector.buttonOptions.forEach(function (rangeOptions, i) {
            buttons[i][verb]({ x: buttonLeft });
            // increase button position for the next button
            buttonLeft += buttons[i].width + pick(options.buttonSpacing, 5);
        });
        plotLeft = chart.plotLeft - chart.spacing[3];
        rangeSelector.updateButtonStates();
        // detect collisiton with exporting
        if (navButtonOptions &&
            this.titleCollision(chart) &&
            verticalAlign === 'top' &&
            buttonPosition.align === 'right' && ((buttonPosition.y +
            buttonGroup.getBBox().height - 12) <
            ((navButtonOptions.y || 0) +
                navButtonOptions.height))) {
            exportingX = -40;
        }
        if (buttonPosition.align === 'left') {
            translateX = buttonPosition.x - chart.spacing[3];
        }
        else if (buttonPosition.align === 'right') {
            translateX =
                buttonPosition.x + exportingX - chart.spacing[1];
        }
        // align button group
        buttonGroup.align({
            y: buttonPosition.y,
            width: buttonGroup.getBBox().width,
            align: buttonPosition.align,
            x: translateX
        }, true, chart.spacingBox);
        // skip animation
        rangeSelector.group.placed = animate;
        rangeSelector.buttonGroup.placed = animate;
        if (inputEnabled !== false) {
            var inputGroupX, inputGroupWidth, buttonGroupX, buttonGroupWidth;
            // detect collision with exporting
            if (navButtonOptions &&
                this.titleCollision(chart) &&
                verticalAlign === 'top' &&
                inputPosition.align === 'right' && ((inputPosition.y -
                inputGroup.getBBox().height - 12) <
                ((navButtonOptions.y || 0) +
                    navButtonOptions.height +
                    chart.spacing[0]))) {
                exportingX = -40;
            }
            else {
                exportingX = 0;
            }
            if (inputPosition.align === 'left') {
                translateX = plotLeft;
            }
            else if (inputPosition.align === 'right') {
                translateX = -Math.max(chart.axisOffset[1], -exportingX);
            }
            // Update the alignment to the updated spacing box
            inputGroup.align({
                y: inputPosition.y,
                width: inputGroup.getBBox().width,
                align: inputPosition.align,
                // fix wrong getBBox() value on right align
                x: inputPosition.x + translateX - 2
            }, true, chart.spacingBox);
            // detect collision
            inputGroupX = (inputGroup.alignAttr.translateX +
                inputGroup.alignOptions.x -
                exportingX +
                // getBBox for detecing left margin
                inputGroup.getBBox().x +
                // 2px padding to not overlap input and label
                2);
            inputGroupWidth = inputGroup.alignOptions.width;
            buttonGroupX = buttonGroup.alignAttr.translateX +
                buttonGroup.getBBox().x;
            // 20 is minimal spacing between elements
            buttonGroupWidth = buttonGroup.getBBox().width + 20;
            if ((inputPosition.align ===
                buttonPosition.align) || ((buttonGroupX + buttonGroupWidth > inputGroupX) &&
                (inputGroupX + inputGroupWidth > buttonGroupX) &&
                (buttonPositionY <
                    (inputPositionY +
                        inputGroup.getBBox().height)))) {
                inputGroup.attr({
                    translateX: inputGroup.alignAttr.translateX +
                        (chart.axisOffset[1] >= -exportingX ? 0 : -exportingX),
                    translateY: inputGroup.alignAttr.translateY +
                        buttonGroup.getBBox().height + 10
                });
            }
            // Set or reset the input values
            rangeSelector.setInputValue('min', min);
            rangeSelector.setInputValue('max', max);
            // skip animation
            rangeSelector.inputGroup.placed = animate;
        }
        // vertical align
        rangeSelector.group.align({
            verticalAlign: verticalAlign
        }, true, chart.spacingBox);
        // set position
        groupHeight =
            rangeSelector.group.getBBox().height + 20; // # 20 padding
        alignTranslateY =
            rangeSelector.group.alignAttr.translateY;
        // calculate bottom position
        if (verticalAlign === 'bottom') {
            legendHeight = (legendOptions &&
                legendOptions.verticalAlign === 'bottom' &&
                legendOptions.enabled &&
                !legendOptions.floating ?
                legend.legendHeight + pick(legendOptions.margin, 10) :
                0);
            groupHeight = groupHeight + legendHeight - 20;
            translateY = (alignTranslateY -
                groupHeight -
                (floating ? 0 : options.y) -
                (chart.titleOffset ? chart.titleOffset[2] : 0) -
                10 // 10 spacing
            );
        }
        if (verticalAlign === 'top') {
            if (floating) {
                translateY = 0;
            }
            if (chart.titleOffset && chart.titleOffset[0]) {
                translateY = chart.titleOffset[0];
            }
            translateY += ((chart.margin[0] - chart.spacing[0]) || 0);
        }
        else if (verticalAlign === 'middle') {
            if (inputPositionY === buttonPositionY) {
                if (inputPositionY < 0) {
                    translateY = alignTranslateY + minPosition;
                }
                else {
                    translateY = alignTranslateY;
                }
            }
            else if (inputPositionY || buttonPositionY) {
                if (inputPositionY < 0 ||
                    buttonPositionY < 0) {
                    translateY -= Math.min(inputPositionY, buttonPositionY);
                }
                else {
                    translateY =
                        alignTranslateY - groupHeight + minPosition;
                }
            }
        }
        rangeSelector.group.translate(options.x, options.y + Math.floor(translateY));
        // translate HTML inputs
        if (inputEnabled !== false) {
            rangeSelector.minInput.style.marginTop =
                rangeSelector.group.translateY + 'px';
            rangeSelector.maxInput.style.marginTop =
                rangeSelector.group.translateY + 'px';
        }
        rangeSelector.rendered = true;
    },
    /**
     * Extracts height of range selector
     *
     * @private
     * @function Highcharts.RangeSelector#getHeight
     * @return {number}
     *         Returns rangeSelector height
     */
    getHeight: function () {
        var rangeSelector = this, options = rangeSelector.options, rangeSelectorGroup = rangeSelector.group, inputPosition = options.inputPosition, buttonPosition = options.buttonPosition, yPosition = options.y, buttonPositionY = buttonPosition.y, inputPositionY = inputPosition.y, rangeSelectorHeight = 0, minPosition;
        if (options.height) {
            return options.height;
        }
        rangeSelectorHeight = rangeSelectorGroup ?
            // 13px to keep back compatibility
            (rangeSelectorGroup.getBBox(true).height) + 13 +
                yPosition :
            0;
        minPosition = Math.min(inputPositionY, buttonPositionY);
        if ((inputPositionY < 0 && buttonPositionY < 0) ||
            (inputPositionY > 0 && buttonPositionY > 0)) {
            rangeSelectorHeight += Math.abs(minPosition);
        }
        return rangeSelectorHeight;
    },
    /**
     * Detect collision with title or subtitle
     *
     * @private
     * @function Highcharts.RangeSelector#titleCollision
     *
     * @param {Highcharts.Chart} chart
     *
     * @return {boolean}
     *         Returns collision status
     */
    titleCollision: function (chart) {
        return !(chart.options.title.text ||
            chart.options.subtitle.text);
    },
    /**
     * Update the range selector with new options
     *
     * @private
     * @function Highcharts.RangeSelector#update
     * @param {Highcharts.RangeSelectorOptions} options
     * @return {void}
     */
    update: function (options) {
        var chart = this.chart;
        merge(true, chart.options.rangeSelector, options);
        this.destroy();
        this.init(chart);
        chart.rangeSelector.render();
    },
    /**
     * Destroys allocated elements.
     *
     * @private
     * @function Highcharts.RangeSelector#destroy
     */
    destroy: function () {
        var rSelector = this, minInput = rSelector.minInput, maxInput = rSelector.maxInput;
        rSelector.unMouseDown();
        rSelector.unResize();
        // Destroy elements in collections
        destroyObjectProperties(rSelector.buttons);
        // Clear input element events
        if (minInput) {
            minInput.onfocus = minInput.onblur = minInput.onchange = null;
        }
        if (maxInput) {
            maxInput.onfocus = maxInput.onblur = maxInput.onchange = null;
        }
        // Destroy HTML and SVG elements
        objectEach(rSelector, function (val, key) {
            if (val && key !== 'chart') {
                if (val.destroy) {
                    // SVGElement
                    val.destroy();
                }
                else if (val.nodeType) {
                    // HTML element
                    discardElement(this[key]);
                }
            }
            if (val !== RangeSelector.prototype[key]) {
                rSelector[key] = null;
            }
        }, this);
    }
};
/**
 * Get the axis min value based on the range option and the current max. For
 * stock charts this is extended via the {@link RangeSelector} so that if the
 * selected range is a multiple of months or years, it is compensated for
 * various month lengths.
 *
 * @private
 * @function Highcharts.Axis#minFromRange
 * @return {number|undefined}
 *         The new minimum value.
 */
Axis.prototype.minFromRange = function () {
    var rangeOptions = this.range, type = rangeOptions.type, timeName = {
        month: 'Month',
        year: 'FullYear'
    }[type], min, max = this.max, dataMin, range, time = this.chart.time, 
    // Get the true range from a start date
    getTrueRange = function (base, count) {
        var date = new time.Date(base), basePeriod = time.get(timeName, date);
        time.set(timeName, date, basePeriod + count);
        if (basePeriod === time.get(timeName, date)) {
            time.set('Date', date, 0); // #6537
        }
        return date.getTime() - base;
    };
    if (isNumber(rangeOptions)) {
        min = max - rangeOptions;
        range = rangeOptions;
    }
    else {
        min = max + getTrueRange(max, -rangeOptions.count);
        // Let the fixedRange reflect initial settings (#5930)
        if (this.chart) {
            this.chart.fixedRange = max - min;
        }
    }
    dataMin = pick(this.dataMin, Number.MIN_VALUE);
    if (!isNumber(min)) {
        min = dataMin;
    }
    if (min <= dataMin) {
        min = dataMin;
        if (typeof range === 'undefined') { // #4501
            range = getTrueRange(min, rangeOptions.count);
        }
        this.newMax = Math.min(min + range, this.dataMax);
    }
    if (!isNumber(max)) {
        min = void 0;
    }
    return min;
};
if (!H.RangeSelector) {
    // Initialize rangeselector for stock charts
    addEvent(Chart, 'afterGetContainer', function () {
        if (this.options.rangeSelector.enabled) {
            this.rangeSelector = new RangeSelector(this);
        }
    });
    addEvent(Chart, 'beforeRender', function () {
        var chart = this, axes = chart.axes, rangeSelector = chart.rangeSelector, verticalAlign;
        if (rangeSelector) {
            if (isNumber(rangeSelector.deferredYTDClick)) {
                rangeSelector.clickButton(rangeSelector.deferredYTDClick);
                delete rangeSelector.deferredYTDClick;
            }
            axes.forEach(function (axis) {
                axis.updateNames();
                axis.setScale();
            });
            chart.getAxisMargins();
            rangeSelector.render();
            verticalAlign = rangeSelector.options.verticalAlign;
            if (!rangeSelector.options.floating) {
                if (verticalAlign === 'bottom') {
                    this.extraBottomMargin = true;
                }
                else if (verticalAlign !== 'middle') {
                    this.extraTopMargin = true;
                }
            }
        }
    });
    addEvent(Chart, 'update', function (e) {
        var chart = this, options = e.options, optionsRangeSelector = options.rangeSelector, rangeSelector = chart.rangeSelector, verticalAlign, extraBottomMarginWas = this.extraBottomMargin, extraTopMarginWas = this.extraTopMargin;
        if (optionsRangeSelector &&
            optionsRangeSelector.enabled &&
            !defined(rangeSelector)) {
            this.options.rangeSelector.enabled = true;
            this.rangeSelector = new RangeSelector(this);
        }
        this.extraBottomMargin = false;
        this.extraTopMargin = false;
        if (rangeSelector) {
            rangeSelector.render();
            verticalAlign = (optionsRangeSelector &&
                optionsRangeSelector.verticalAlign) || (rangeSelector.options && rangeSelector.options.verticalAlign);
            if (!rangeSelector.options.floating) {
                if (verticalAlign === 'bottom') {
                    this.extraBottomMargin = true;
                }
                else if (verticalAlign !== 'middle') {
                    this.extraTopMargin = true;
                }
            }
            if (this.extraBottomMargin !== extraBottomMarginWas ||
                this.extraTopMargin !== extraTopMarginWas) {
                this.isDirtyBox = true;
            }
        }
    });
    addEvent(Chart, 'render', function () {
        var chart = this, rangeSelector = chart.rangeSelector, verticalAlign;
        if (rangeSelector && !rangeSelector.options.floating) {
            rangeSelector.render();
            verticalAlign = rangeSelector.options.verticalAlign;
            if (verticalAlign === 'bottom') {
                this.extraBottomMargin = true;
            }
            else if (verticalAlign !== 'middle') {
                this.extraTopMargin = true;
            }
        }
    });
    addEvent(Chart, 'getMargins', function () {
        var rangeSelector = this.rangeSelector, rangeSelectorHeight;
        if (rangeSelector) {
            rangeSelectorHeight = rangeSelector.getHeight();
            if (this.extraTopMargin) {
                this.plotTop += rangeSelectorHeight;
            }
            if (this.extraBottomMargin) {
                this.marginBottom += rangeSelectorHeight;
            }
        }
    });
    Chart.prototype.callbacks.push(function (chart) {
        var extremes, rangeSelector = chart.rangeSelector, unbindRender, unbindSetExtremes;
        /**
         * @private
         */
        function renderRangeSelector() {
            extremes = chart.xAxis[0].getExtremes();
            if (isNumber(extremes.min)) {
                rangeSelector.render(extremes.min, extremes.max);
            }
        }
        if (rangeSelector) {
            // redraw the scroller on setExtremes
            unbindSetExtremes = addEvent(chart.xAxis[0], 'afterSetExtremes', function (e) {
                rangeSelector.render(e.min, e.max);
            });
            // redraw the scroller chart resize
            unbindRender = addEvent(chart, 'redraw', renderRangeSelector);
            // do it now
            renderRangeSelector();
        }
        // Remove resize/afterSetExtremes at chart destroy
        addEvent(chart, 'destroy', function destroyEvents() {
            if (rangeSelector) {
                unbindRender();
                unbindSetExtremes();
            }
        });
    });
    H.RangeSelector = RangeSelector;
}