/** * (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