/** * (c) 2010-2019 Torstein Honsi * * License: www.highcharts.com/license */ /** * 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. */ 'use strict'; import H from './Globals.js'; 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, defined = H.defined, destroyObjectProperties = H.destroyObjectProperties, discardElement = H.discardElement, extend = H.extend, fireEvent = H.fireEvent, isNumber = H.isNumber, merge = H.merge, pick = H.pick, pInt = H.pInt, splat = H.splat; /* **************************************************************************** * 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 * @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 * *
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. * *
         * 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`, `ytd`, `all`. * * @type {string} * @validvalue ["millisecond", "second", "minute", "day", "week", "month", "ytd", "all"] * @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.VerticalAlignType} * @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: undefined, // reserved space for buttons and input /** * 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.AlignType} * @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} * @product highstock * @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 * * @since 6.0.0 * @validvalue ["left", "center", "right"] */ 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. * *
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 */ rangeSelectorZoom: 'Zoom', /** * The text for the label for the "from" input box in the range * selector. * * @product highstock */ rangeSelectorFrom: 'From', /** * The text for the label for the "to" input box in the range selector. * * @product highstock */ rangeSelectorTo: 'To' } ); /** * 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 */ 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 (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 {boolean} selected */ 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 */ 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 = []; chart.extraTopMargin = options.height; 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 (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 */ 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.RangeSelectorOptions} rangeOptions */ 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 */ 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 */ 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 */ 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 */ 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; 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 = undefined; } else if (value < dataMin) { value = dataMin; } } else { if (value < rangeSelector.minInput.HCTime) { value = undefined; } else if (value > dataMax) { value = dataMax; } } // Set the extremes if (value !== undefined) { chartAxis.setExtremes( isMin ? value : chartAxis.min, isMin ? chartAxis.max : value, undefined, undefined, { 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', // Chrome needs a pixel to see it 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 () { if (input === H.doc.activeElement) { // Only when focused // Update also when no `change` event is triggered, like when // clicking inside the SVG (#4710) updateExtremes(); rangeSelector.hideInput(name); } }; // 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} */ 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 */ 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 () { // extract events from button object and call var buttonEvents = ( rangeOptions.events && rangeOptions.events.click ), callDefaultEvent; if (buttonEvents) { callDefaultEvent = buttonEvents.call(rangeOptions); } 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) - 10 // 10 spacing ); } if (verticalAlign === 'top') { if (floating) { translateY = 0; } if (chart.titleOffset) { translateY = chart.titleOffset + chart.options.title.margin; } 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; 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 */ 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 H.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); } }; /** * Add logic to normalize the zoomed range in order to preserve the pressed * state of range selector buttons * * @private * @function Highcharts.Axis#toFixedRange * * @param {number} pxMin * * @param {number} pxMax * * @param {number} fixedMin * * @param {number} fixedMax * * @return {*} */ Axis.prototype.toFixedRange = function (pxMin, pxMax, fixedMin, fixedMax) { var fixedRange = this.chart && this.chart.fixedRange, newMin = pick(fixedMin, this.translate(pxMin, true, !this.horiz)), newMax = pick(fixedMax, this.translate(pxMax, true, !this.horiz)), changeRatio = fixedRange && (newMax - newMin) / fixedRange; // If the difference between the fixed range and the actual requested range // is too great, the user is dragging across an ordinal gap, and we need to // release the range selector button. if (changeRatio > 0.7 && changeRatio < 1.3) { if (fixedMax) { newMin = newMax - fixedRange; } else { newMax = newMin + fixedRange; } } if (!isNumber(newMin) || !isNumber(newMax)) { // #1195, #7411 newMin = newMax = undefined; } return { min: newMin, max: newMax }; }; /** * 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} * 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, // Get the true range from a start date getTrueRange = function (base, count) { var date = new Date(base), basePeriod = date['get' + timeName](); date['set' + timeName](basePeriod + count); if (basePeriod === date['get' + timeName]()) { date.setDate(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 (range === undefined) { // #4501 range = getTrueRange(min, rangeOptions.count); } this.newMax = Math.min(min + range, this.dataMax); } if (!isNumber(max)) { min = undefined; } return min; }; // 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; 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; /* **************************************************************************** * End Range Selector code * *****************************************************************************/