highcharts
Version:
JavaScript charting framework
1,298 lines • 84.5 kB
JavaScript
/* *
*
* (c) 2010-2021 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import Axis from '../Core/Axis/Axis.js';
import Chart from '../Core/Chart/Chart.js';
import H from '../Core/Globals.js';
import O from '../Core/Options.js';
var defaultOptions = O.defaultOptions;
import palette from '../Core/Color/Palette.js';
import SVGElement from '../Core/Renderer/SVG/SVGElement.js';
import U from '../Core/Utilities.js';
var addEvent = U.addEvent, createElement = U.createElement, css = U.css, defined = U.defined, destroyObjectProperties = U.destroyObjectProperties, discardElement = U.discardElement, extend = U.extend, find = U.find, fireEvent = U.fireEvent, isNumber = U.isNumber, merge = U.merge, objectEach = U.objectEach, pad = U.pad, pick = U.pick, pInt = U.pInt, splat = U.splat;
/**
* 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.
*/
/* ************************************************************************** *
* 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
*
* @since 2.0.3
*/
allButtonsEnabled: false,
/**
* An array of configuration objects for the buttons.
*
* Defaults to:
* ```js
* buttons: [{
* type: 'month',
* count: 1,
* text: '1m',
* title: 'View 1 month'
* }, {
* type: 'month',
* count: 3,
* text: '3m',
* title: 'View 3 months'
* }, {
* type: 'month',
* count: 6,
* text: '6m',
* title: 'View 6 months'
* }, {
* type: 'ytd',
* text: 'YTD',
* title: 'View year to date'
* }, {
* type: 'year',
* count: 1,
* text: '1y',
* title: 'View 1 year'
* }, {
* type: 'all',
* text: 'All',
* title: 'View all'
* }]
* ```
*
* @sample {highstock} stock/rangeselector/datagrouping/
* Data grouping by buttons
*
* @type {Array<*>}
*/
buttons: void 0,
/**
* 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
*/
/**
* Explanation for the button, shown as a tooltip on hover, and used by
* assistive technology.
*
* @type {string}
* @apioption rangeSelector.buttons.title
*/
/**
* 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.
*/
buttonSpacing: 5,
/**
* Whether to collapse the range selector buttons into a dropdown when
* there is not enough room to show everything in a single row, instead
* of dividing the range selector into multiple rows.
* Can be one of the following:
* - `always`: Always collapse
* - `responsive`: Only collapse when there is not enough room
* - `never`: Never collapse
*
* @sample {highstock} stock/rangeselector/dropdown/
* Dropdown option
*
* @validvalue ["always", "responsive", "never"]
* @since 9.0.0
*/
dropdown: 'responsive',
/**
* Enable or disable the range selector. Default to `true` for stock
* charts, using the `stockChart` factory.
*
* @sample {highstock} stock/rangeselector/enabled/
* Disable the range selector
*
* @type {boolean|undefined}
* @default {highstock} true
*/
enabled: void 0,
/**
* 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}
* @since 1.3.7
*/
inputBoxBorderColor: 'none',
/**
* The pixel height of the date input boxes.
*
* @sample {highstock} stock/rangeselector/styling/
* Styling the buttons and inputs
*
* @since 1.3.7
*/
inputBoxHeight: 17,
/**
* The pixel width of the date input boxes. When `undefined`, the width
* is fitted to the rendered content.
*
* @sample {highstock} stock/rangeselector/styling/
* Styling the buttons and inputs
*
* @type {number|undefined}
* @since 1.3.7
*/
inputBoxWidth: void 0,
/**
* The date format in the input boxes when not selected for editing.
* Defaults to `%b %e, %Y`.
*
* This is used to determine which type of input to show,
* `datetime-local`, `date` or `time` and falling back to `text` when
* the browser does not support the input type or the format contains
* milliseconds.
*
* @sample {highstock} stock/rangeselector/input-type/
* Input types
* @sample {highstock} stock/rangeselector/input-format/
* Milliseconds in the range selector
*
*/
inputDateFormat: '%b %e, %Y',
/**
* A custom callback function to parse values entered in the input boxes
* and return a valid JavaScript time as milliseconds since 1970.
* The first argument passed is a value to parse,
* second is a boolean indicating use of the UTC time.
*
* This will only get called for inputs of type `text`. Since v8.2.3,
* the input type is dynamically determined based on the granularity
* of the `inputDateFormat` and the browser support.
*
* @sample {highstock} stock/rangeselector/input-format/
* Milliseconds in the range selector
*
* @type {Highcharts.RangeSelectorParseCallbackFunction}
* @since 1.3.3
*/
inputDateParser: void 0,
/**
* 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.
*
* This will only be used for inputs of type `text`. Since v8.2.3,
* the input type is dynamically determined based on the granularity
* of the `inputDateFormat` and the browser support.
*
* @sample {highstock} stock/rangeselector/input-format/
* Milliseconds in the range selector
*
*/
inputEditDateFormat: '%Y-%m-%d',
/**
* Enable or disable the date input boxes.
*/
inputEnabled: true,
/**
* 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 space in pixels between the labels and the date input boxes in
* the range selector.
*
* @since 9.0.0
*/
inputSpacing: 5,
/**
* The index of the button to appear pre-selected.
*
* @type {number}
*/
selected: void 0,
/**
* 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
*/
inputStyle: {
/** @ignore */
color: palette.highlightColor80,
/** @ignore */
cursor: 'pointer'
},
/**
* 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: palette.neutralColor60
}
}
});
extend(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. Since v9.0, this string is empty as the label is not
* rendered by default.
*
* @product highstock gantt
*/
rangeSelectorFrom: '',
/**
* The text for the label for the "to" input box in the range selector.
*
* @product highstock gantt
*/
rangeSelectorTo: '→'
});
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* The range selector.
*
* @private
* @class
* @name Highcharts.RangeSelector
* @param {Highcharts.Chart} chart
*/
var RangeSelector = /** @class */ (function () {
function RangeSelector(chart) {
/* *
*
* Properties
*
* */
this.buttons = void 0;
this.buttonOptions = RangeSelector.prototype.defaultButtons;
this.initialButtonGroupWidth = 0;
this.options = void 0;
this.chart = chart;
// Run RangeSelector
this.init(chart);
}
/**
* 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}
*/
RangeSelector.prototype.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;
}
if (defined(newMin)) {
newMin += rangeOptions._offsetMin;
}
if (defined(newMax)) {
newMax += rangeOptions._offsetMax;
}
rangeSelector.setSelected(i);
if (this.dropdown) {
this.dropdown.selectedIndex = i + 1;
}
// 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, true), void 0, // auto animation
{
trigger: 'rangeSelectorButton',
rangeSelectorButton: rangeOptions
});
}
fireEvent(this, 'afterBtnClick');
};
/**
* 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}
*/
RangeSelector.prototype.setSelected = function (selected) {
this.selected = this.options.selected = selected;
};
/**
* Initialize the range selector
*
* @private
* @function Highcharts.RangeSelector#init
* @param {Highcharts.Chart} chart
* @return {void}
*/
RangeSelector.prototype.init = function (chart) {
var rangeSelector = this, options = chart.options.rangeSelector, buttonOptions = options.buttons || rangeSelector.defaultButtons.slice(), 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.eventsToUnbind = [];
this.eventsToUnbind.push(addEvent(chart.container, 'mousedown', blurInputs));
this.eventsToUnbind.push(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);
}
this.eventsToUnbind.push(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}
*/
RangeSelector.prototype.updateButtonStates = function () {
var rangeSelector = this, chart = this.chart, dropdown = this.dropdown, 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);
if (dropdown) {
dropdown.options[i + 1].disabled = disable;
if (state === 2) {
dropdown.selectedIndex = i + 1;
}
}
// Reset (#9209)
if (state === 0 && selected === i) {
rangeSelector.setSelected();
}
}
});
};
/**
* Compute and cache the range for an individual button
*
* @private
* @function Highcharts.RangeSelector#computeButtonRange
* @param {Highcharts.RangeSelectorButtonsOptions} rangeOptions
* @return {void}
*/
RangeSelector.prototype.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;
};
/**
* Get the unix timestamp of a HTML input for the dates
*
* @private
* @function Highcharts.RangeSelector#getInputValue
* @param {string} name
* @return {number}
*/
RangeSelector.prototype.getInputValue = function (name) {
var input = name === 'min' ? this.minInput : this.maxInput;
var options = this.chart.options.rangeSelector;
var time = this.chart.time;
if (input) {
return ((input.type === 'text' && options.inputDateParser) ||
this.defaultInputDateParser)(input.value, time.useUTC, time);
}
return 0;
};
/**
* 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}
*/
RangeSelector.prototype.setInputValue = function (name, inputTime) {
var options = this.options, time = this.chart.time, input = name === 'min' ? this.minInput : this.maxInput, dateBox = name === 'min' ? this.minDateBox : this.maxDateBox;
if (input) {
var hcTimeAttr = input.getAttribute('data-hc-time');
var updatedTime = defined(hcTimeAttr) ? Number(hcTimeAttr) : void 0;
if (defined(inputTime)) {
var previousTime = updatedTime;
if (defined(previousTime)) {
input.setAttribute('data-hc-time-previous', previousTime);
}
input.setAttribute('data-hc-time', inputTime);
updatedTime = inputTime;
}
input.value = time.dateFormat(this.inputTypeFormats[input.type] || options.inputEditDateFormat, updatedTime);
if (dateBox) {
dateBox.attr({
text: time.dateFormat(options.inputDateFormat, updatedTime)
});
}
}
};
/**
* Set the min and max value of a HTML input for the dates
*
* @private
* @function Highcharts.RangeSelector#setInputExtremes
* @param {string} name
* @param {number} min
* @param {number} max
* @return {void}
*/
RangeSelector.prototype.setInputExtremes = function (name, min, max) {
var input = name === 'min' ? this.minInput : this.maxInput;
if (input) {
var format = this.inputTypeFormats[input.type];
var time = this.chart.time;
if (format) {
var newMin = time.dateFormat(format, min);
if (input.min !== newMin) {
input.min = newMin;
}
var newMax = time.dateFormat(format, max);
if (input.max !== newMax) {
input.max = newMax;
}
}
}
};
/**
* @private
* @function Highcharts.RangeSelector#showInput
* @param {string} name
* @return {void}
*/
RangeSelector.prototype.showInput = function (name) {
var dateBox = name === 'min' ? this.minDateBox : this.maxDateBox;
var input = name === 'min' ? this.minInput : this.maxInput;
if (input && dateBox && this.inputGroup) {
var isTextInput = input.type === 'text';
var _a = this.inputGroup, translateX = _a.translateX, translateY = _a.translateY;
css(input, {
width: isTextInput ? ((dateBox.width - 2) + 'px') : 'auto',
height: isTextInput ? ((dateBox.height - 2) + 'px') : 'auto',
border: '2px solid silver'
});
if (isTextInput) {
css(input, {
left: (translateX + dateBox.x) + 'px',
top: translateY + 'px'
});
// Inputs of types date, time or datetime-local should be centered
// on top of the dateBox
}
else {
css(input, {
left: Math.min(Math.round(dateBox.x +
translateX -
(input.offsetWidth - dateBox.width) / 2), this.chart.chartWidth - input.offsetWidth) + 'px',
top: (translateY - (input.offsetHeight - dateBox.height) / 2) + 'px'
});
}
}
};
/**
* @private
* @function Highcharts.RangeSelector#hideInput
* @param {string} name
* @return {void}
*/
RangeSelector.prototype.hideInput = function (name) {
var input = name === 'min' ? this.minInput : this.maxInput;
if (input) {
css(input, {
top: '-9999em',
border: 0,
width: '1px',
height: '1px'
});
}
};
/**
* @private
* @function Highcharts.RangeSelector#defaultInputDateParser
*/
RangeSelector.prototype.defaultInputDateParser = function (inputDate, useUTC, time) {
var hasTimezone = function (str) {
return str.length > 6 &&
(str.lastIndexOf('-') === str.length - 6 ||
str.lastIndexOf('+') === str.length - 6);
};
var input = inputDate.split('/').join('-').split(' ').join('T');
if (input.indexOf('T') === -1) {
input += 'T00:00';
}
if (useUTC) {
input += 'Z';
}
else if (H.isSafari && !hasTimezone(input)) {
var offset = new Date(input).getTimezoneOffset() / 60;
input += offset <= 0 ? "+" + pad(-offset) + ":00" : "-" + pad(offset) + ":00";
}
var date = Date.parse(input);
// If the value isn't parsed directly to a value by the
// browser's Date.parse method, like YYYY-MM-DD in IE8, try
// parsing it a different way
if (!isNumber(date)) {
var parts = inputDate.split('-');
date = Date.UTC(pInt(parts[0]), pInt(parts[1]) - 1, pInt(parts[2]));
}
if (time && useUTC) {
date += time.getTimezoneOffset(date);
}
return date;
};
/**
* Draw either the 'from' or the 'to' HTML input box of the range selector
*
* @private
* @function Highcharts.RangeSelector#drawInput
* @param {string} name
* @return {RangeSelectorInputElements}
*/
RangeSelector.prototype.drawInput = function (name) {
var _a = this, chart = _a.chart, div = _a.div, inputGroup = _a.inputGroup;
var rangeSelector = this, chartStyle = chart.renderer.style || {}, renderer = chart.renderer, options = chart.options.rangeSelector, lang = defaultOptions.lang, isMin = name === 'min';
/**
* @private
*/
function updateExtremes() {
var value = rangeSelector.getInputValue(name), chartAxis = chart.xAxis[0], dataAxis = chart.scroller && chart.scroller.xAxis ?
chart.scroller.xAxis :
chartAxis, dataMin = dataAxis.dataMin, dataMax = dataAxis.dataMax;
var maxInput = rangeSelector.maxInput, minInput = rangeSelector.minInput;
if (value !== Number(input.getAttribute('data-hc-time-previous')) &&
isNumber(value)) {
input.setAttribute('data-hc-time-previous', value);
// Validate the extremes. If it goes beyound the data min or
// max, use the actual data extreme (#2438).
if (isMin && maxInput && isNumber(dataMin)) {
if (value > Number(maxInput.getAttribute('data-hc-time'))) {
value = void 0;
}
else if (value < dataMin) {
value = dataMin;
}
}
else if (minInput && isNumber(dataMax)) {
if (value < Number(minInput.getAttribute('data-hc-time'))) {
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
var text = lang[isMin ? 'rangeSelectorFrom' : 'rangeSelectorTo'];
var label = renderer
.label(text, 0)
.addClass('highcharts-range-label')
.attr({
padding: text ? 2 : 0
})
.add(inputGroup);
// Create an SVG label that shows updated date ranges and and records
// click events that bring in the HTML input.
var dateBox = renderer
.label('', 0)
.addClass('highcharts-range-input')
.attr({
padding: 2,
width: options.inputBoxWidth,
height: options.inputBoxHeight,
'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,
'stroke-width': 1
});
}
dateBox.add(inputGroup);
// Create the HTML input element. This is rendered as 1x1 pixel then set
// to the right size when focused.
var input = createElement('input', {
name: name,
className: 'highcharts-range-selector'
}, void 0, div);
// #14788: Setting input.type to an unsupported type throws in IE, so
// we need to use setAttribute instead
input.setAttribute('type', preferredInputType(options.inputDateFormat || '%b %e, %Y'));
if (!chart.styledMode) {
// Styles
label.css(merge(chartStyle, options.labelStyle));
dateBox.css(merge({
color: palette.neutralColor80
}, chartStyle, options.inputStyle));
css(input, extend({
position: 'absolute',
border: 0,
boxShadow: '0 0 15px rgba(0,0,0,0.3)',
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);
rangeSelector.setInputValue(name);
input.blur(); // #4606
};
var keyDown = false;
// handle changes in the input boxes
input.onchange = function () {
updateExtremes();
// Blur input when clicking date input calendar
if (!keyDown) {
rangeSelector.hideInput(name);
input.blur();
}
};
input.onkeypress = function (event) {
// IE does not fire onchange on enter
if (event.keyCode === 13) {
updateExtremes();
}
};
input.onkeydown = function () {
keyDown = true;
};
input.onkeyup = function () {
keyDown = false;
};
return { dateBox: dateBox, input: input, label: label };
};
/**
* 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>}
*/
RangeSelector.prototype.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
*/
RangeSelector.prototype.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, startOfYear);
var ts = now.getTime();
return {
max: Math.min(dataMax || ts, ts),
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}
*/
RangeSelector.prototype.render = function (min, max) {
var chart = this.chart, renderer = chart.renderer, container = chart.container, chartOptions = chart.options, options = chartOptions.rangeSelector,
// Place inputs above the container
inputsZIndex = pick(chartOptions.chart.style &&
chartOptions.chart.style.zIndex, 0) + 1, inputEnabled = options.inputEnabled, rendered = this.rendered;
if (options.enabled === false) {
return;
}
// create the elements
if (!rendered) {
this.group = renderer.g('range-selector-group')
.attr({
zIndex: 7
})
.add();
this.div = createElement('div', void 0, {
position: 'relative',
height: 0,
zIndex: inputsZIndex
});
if (this.buttonOptions.length) {
this.renderButtons();
}
// First create a wrapper outside the container in order to make
// the inputs work and make export correct
if (container.parentNode) {
container.parentNode.insertBefore(this.div, container);
}
if (inputEnabled) {
// Create the group to keep the inputs
this.inputGroup = renderer.g('input-group').add(this.group);
var minElems = this.drawInput('min');
this.minDateBox = minElems.dateBox;
this.minLabel = minElems.label;
this.minInput = minElems.input;
var maxElems = this.drawInput('max');
this.maxDateBox = maxElems.dateBox;
this.maxLabel = maxElems.label;
this.maxInput = maxElems.input;
}
}
if (inputEnabled) {
// Set or reset the input values
this.setInputValue('min', min);
this.setInputValue('max', max);
var unionExtremes = (chart.scroller && chart.scroller.getUnionExtremes()) || chart.xAxis[0] || {};
if (defined(unionExtremes.dataMin) && defined(unionExtremes.dataMax)) {
var minRange = chart.xAxis[0].minRange || 0;
this.setInputExtremes('min', unionExtremes.dataMin, Math.min(unionExtremes.dataMax, this.getInputValue('max')) - minRange);
this.setInputExtremes('max', Math.max(unionExtremes.dataMin, this.getInputValue('min')) + minRange, unionExtremes.dataMax);
}
// Reflow
if (this.inputGroup) {
var x_1 = 0;
[
this.minLabel,
this.minDateBox,
this.maxLabel,
this.maxDateBox
].forEach(functio