RangeSelector.js 62 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630
  1. /* *
  2. *
  3. * (c) 2010-2020 Torstein Honsi
  4. *
  5. * License: www.highcharts.com/license
  6. *
  7. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  8. *
  9. * */
  10. 'use strict';
  11. import Axis from '../Core/Axis/Axis.js';
  12. import Chart from '../Core/Chart/Chart.js';
  13. import H from '../Core/Globals.js';
  14. import O from '../Core/Options.js';
  15. var defaultOptions = O.defaultOptions;
  16. import SVGElement from '../Core/Renderer/SVG/SVGElement.js';
  17. import U from '../Core/Utilities.js';
  18. var addEvent = U.addEvent, createElement = U.createElement, css = U.css, defined = U.defined, destroyObjectProperties = U.destroyObjectProperties, discardElement = U.discardElement, extend = U.extend, fireEvent = U.fireEvent, isNumber = U.isNumber, merge = U.merge, objectEach = U.objectEach, pick = U.pick, pInt = U.pInt, splat = U.splat;
  19. /**
  20. * Define the time span for the button
  21. *
  22. * @typedef {"all"|"day"|"hour"|"millisecond"|"minute"|"month"|"second"|"week"|"year"|"ytd"} Highcharts.RangeSelectorButtonTypeValue
  23. */
  24. /**
  25. * Callback function to react on button clicks.
  26. *
  27. * @callback Highcharts.RangeSelectorClickCallbackFunction
  28. *
  29. * @param {global.Event} e
  30. * Event arguments.
  31. *
  32. * @param {boolean|undefined}
  33. * Return false to cancel the default button event.
  34. */
  35. /**
  36. * Callback function to parse values entered in the input boxes and return a
  37. * valid JavaScript time as milliseconds since 1970.
  38. *
  39. * @callback Highcharts.RangeSelectorParseCallbackFunction
  40. *
  41. * @param {string} value
  42. * Input value to parse.
  43. *
  44. * @return {number}
  45. * Parsed JavaScript time value.
  46. */
  47. /* ************************************************************************** *
  48. * Start Range Selector code *
  49. * ************************************************************************** */
  50. extend(defaultOptions, {
  51. /**
  52. * The range selector is a tool for selecting ranges to display within
  53. * the chart. It provides buttons to select preconfigured ranges in
  54. * the chart, like 1 day, 1 week, 1 month etc. It also provides input
  55. * boxes where min and max dates can be manually input.
  56. *
  57. * @product highstock gantt
  58. * @optionparent rangeSelector
  59. */
  60. rangeSelector: {
  61. /**
  62. * Whether to enable all buttons from the start. By default buttons are
  63. * only enabled if the corresponding time range exists on the X axis,
  64. * but enabling all buttons allows for dynamically loading different
  65. * time ranges.
  66. *
  67. * @sample {highstock} stock/rangeselector/allbuttonsenabled-true/
  68. * All buttons enabled
  69. *
  70. * @type {boolean}
  71. * @default false
  72. * @since 2.0.3
  73. * @apioption rangeSelector.allButtonsEnabled
  74. */
  75. /**
  76. * An array of configuration objects for the buttons.
  77. *
  78. * Defaults to:
  79. * ```js
  80. * buttons: [{
  81. * type: 'month',
  82. * count: 1,
  83. * text: '1m'
  84. * }, {
  85. * type: 'month',
  86. * count: 3,
  87. * text: '3m'
  88. * }, {
  89. * type: 'month',
  90. * count: 6,
  91. * text: '6m'
  92. * }, {
  93. * type: 'ytd',
  94. * text: 'YTD'
  95. * }, {
  96. * type: 'year',
  97. * count: 1,
  98. * text: '1y'
  99. * }, {
  100. * type: 'all',
  101. * text: 'All'
  102. * }]
  103. * ```
  104. *
  105. * @sample {highstock} stock/rangeselector/datagrouping/
  106. * Data grouping by buttons
  107. *
  108. * @type {Array<*>}
  109. * @apioption rangeSelector.buttons
  110. */
  111. /**
  112. * How many units of the defined type the button should span. If `type`
  113. * is "month" and `count` is 3, the button spans three months.
  114. *
  115. * @type {number}
  116. * @default 1
  117. * @apioption rangeSelector.buttons.count
  118. */
  119. /**
  120. * Fires when clicking on the rangeSelector button. One parameter,
  121. * event, is passed to the function, containing common event
  122. * information.
  123. *
  124. * ```js
  125. * click: function(e) {
  126. * console.log(this);
  127. * }
  128. * ```
  129. *
  130. * Return false to stop default button's click action.
  131. *
  132. * @sample {highstock} stock/rangeselector/button-click/
  133. * Click event on the button
  134. *
  135. * @type {Highcharts.RangeSelectorClickCallbackFunction}
  136. * @apioption rangeSelector.buttons.events.click
  137. */
  138. /**
  139. * Additional range (in milliseconds) added to the end of the calculated
  140. * time span.
  141. *
  142. * @sample {highstock} stock/rangeselector/min-max-offsets/
  143. * Button offsets
  144. *
  145. * @type {number}
  146. * @default 0
  147. * @since 6.0.0
  148. * @apioption rangeSelector.buttons.offsetMax
  149. */
  150. /**
  151. * Additional range (in milliseconds) added to the start of the
  152. * calculated time span.
  153. *
  154. * @sample {highstock} stock/rangeselector/min-max-offsets/
  155. * Button offsets
  156. *
  157. * @type {number}
  158. * @default 0
  159. * @since 6.0.0
  160. * @apioption rangeSelector.buttons.offsetMin
  161. */
  162. /**
  163. * When buttons apply dataGrouping on a series, by default zooming
  164. * in/out will deselect buttons and unset dataGrouping. Enable this
  165. * option to keep buttons selected when extremes change.
  166. *
  167. * @sample {highstock} stock/rangeselector/preserve-datagrouping/
  168. * Different preserveDataGrouping settings
  169. *
  170. * @type {boolean}
  171. * @default false
  172. * @since 6.1.2
  173. * @apioption rangeSelector.buttons.preserveDataGrouping
  174. */
  175. /**
  176. * A custom data grouping object for each button.
  177. *
  178. * @see [series.dataGrouping](#plotOptions.series.dataGrouping)
  179. *
  180. * @sample {highstock} stock/rangeselector/datagrouping/
  181. * Data grouping by range selector buttons
  182. *
  183. * @type {*}
  184. * @extends plotOptions.series.dataGrouping
  185. * @apioption rangeSelector.buttons.dataGrouping
  186. */
  187. /**
  188. * The text for the button itself.
  189. *
  190. * @type {string}
  191. * @apioption rangeSelector.buttons.text
  192. */
  193. /**
  194. * Defined the time span for the button. Can be one of `millisecond`,
  195. * `second`, `minute`, `hour`, `day`, `week`, `month`, `year`, `ytd`,
  196. * and `all`.
  197. *
  198. * @type {Highcharts.RangeSelectorButtonTypeValue}
  199. * @apioption rangeSelector.buttons.type
  200. */
  201. /**
  202. * The space in pixels between the buttons in the range selector.
  203. *
  204. * @type {number}
  205. * @default 0
  206. * @apioption rangeSelector.buttonSpacing
  207. */
  208. /**
  209. * Enable or disable the range selector.
  210. *
  211. * @sample {highstock} stock/rangeselector/enabled/
  212. * Disable the range selector
  213. *
  214. * @type {boolean}
  215. * @default true
  216. * @apioption rangeSelector.enabled
  217. */
  218. /**
  219. * The vertical alignment of the rangeselector box. Allowed properties
  220. * are `top`, `middle`, `bottom`.
  221. *
  222. * @sample {highstock} stock/rangeselector/vertical-align-middle/
  223. * Middle
  224. * @sample {highstock} stock/rangeselector/vertical-align-bottom/
  225. * Bottom
  226. *
  227. * @type {Highcharts.VerticalAlignValue}
  228. * @since 6.0.0
  229. */
  230. verticalAlign: 'top',
  231. /**
  232. * A collection of attributes for the buttons. The object takes SVG
  233. * attributes like `fill`, `stroke`, `stroke-width`, as well as `style`,
  234. * a collection of CSS properties for the text.
  235. *
  236. * The object can also be extended with states, so you can set
  237. * presentational options for `hover`, `select` or `disabled` button
  238. * states.
  239. *
  240. * CSS styles for the text label.
  241. *
  242. * In styled mode, the buttons are styled by the
  243. * `.highcharts-range-selector-buttons .highcharts-button` rule with its
  244. * different states.
  245. *
  246. * @sample {highstock} stock/rangeselector/styling/
  247. * Styling the buttons and inputs
  248. *
  249. * @type {Highcharts.SVGAttributes}
  250. */
  251. buttonTheme: {
  252. /** @ignore */
  253. width: 28,
  254. /** @ignore */
  255. height: 18,
  256. /** @ignore */
  257. padding: 2,
  258. /** @ignore */
  259. zIndex: 7 // #484, #852
  260. },
  261. /**
  262. * When the rangeselector is floating, the plot area does not reserve
  263. * space for it. This opens for positioning anywhere on the chart.
  264. *
  265. * @sample {highstock} stock/rangeselector/floating/
  266. * Placing the range selector between the plot area and the
  267. * navigator
  268. *
  269. * @since 6.0.0
  270. */
  271. floating: false,
  272. /**
  273. * The x offset of the range selector relative to its horizontal
  274. * alignment within `chart.spacingLeft` and `chart.spacingRight`.
  275. *
  276. * @since 6.0.0
  277. */
  278. x: 0,
  279. /**
  280. * The y offset of the range selector relative to its horizontal
  281. * alignment within `chart.spacingLeft` and `chart.spacingRight`.
  282. *
  283. * @since 6.0.0
  284. */
  285. y: 0,
  286. /**
  287. * Deprecated. The height of the range selector. Currently it is
  288. * calculated dynamically.
  289. *
  290. * @deprecated
  291. * @type {number|undefined}
  292. * @since 2.1.9
  293. */
  294. height: void 0,
  295. /**
  296. * The border color of the date input boxes.
  297. *
  298. * @sample {highstock} stock/rangeselector/styling/
  299. * Styling the buttons and inputs
  300. *
  301. * @type {Highcharts.ColorString}
  302. * @default #cccccc
  303. * @since 1.3.7
  304. * @apioption rangeSelector.inputBoxBorderColor
  305. */
  306. /**
  307. * The pixel height of the date input boxes.
  308. *
  309. * @sample {highstock} stock/rangeselector/styling/
  310. * Styling the buttons and inputs
  311. *
  312. * @type {number}
  313. * @default 17
  314. * @since 1.3.7
  315. * @apioption rangeSelector.inputBoxHeight
  316. */
  317. /**
  318. * CSS for the container DIV holding the input boxes. Deprecated as
  319. * of 1.2.5\. Use [inputPosition](#rangeSelector.inputPosition) instead.
  320. *
  321. * @sample {highstock} stock/rangeselector/styling/
  322. * Styling the buttons and inputs
  323. *
  324. * @deprecated
  325. * @type {Highcharts.CSSObject}
  326. * @apioption rangeSelector.inputBoxStyle
  327. */
  328. /**
  329. * The pixel width of the date input boxes.
  330. *
  331. * @sample {highstock} stock/rangeselector/styling/
  332. * Styling the buttons and inputs
  333. *
  334. * @type {number}
  335. * @default 90
  336. * @since 1.3.7
  337. * @apioption rangeSelector.inputBoxWidth
  338. */
  339. /**
  340. * The date format in the input boxes when not selected for editing.
  341. * Defaults to `%b %e, %Y`.
  342. *
  343. * @sample {highstock} stock/rangeselector/input-format/
  344. * Milliseconds in the range selector
  345. *
  346. * @type {string}
  347. * @default %b %e, %Y
  348. * @apioption rangeSelector.inputDateFormat
  349. */
  350. /**
  351. * A custom callback function to parse values entered in the input boxes
  352. * and return a valid JavaScript time as milliseconds since 1970.
  353. * The first argument passed is a value to parse,
  354. * second is a boolean indicating use of the UTC time.
  355. *
  356. * @sample {highstock} stock/rangeselector/input-format/
  357. * Milliseconds in the range selector
  358. *
  359. * @type {Highcharts.RangeSelectorParseCallbackFunction}
  360. * @since 1.3.3
  361. * @apioption rangeSelector.inputDateParser
  362. */
  363. /**
  364. * The date format in the input boxes when they are selected for
  365. * editing. This must be a format that is recognized by JavaScript
  366. * Date.parse.
  367. *
  368. * @sample {highstock} stock/rangeselector/input-format/
  369. * Milliseconds in the range selector
  370. *
  371. * @type {string}
  372. * @default %Y-%m-%d
  373. * @apioption rangeSelector.inputEditDateFormat
  374. */
  375. /**
  376. * Enable or disable the date input boxes. Defaults to enabled when
  377. * there is enough space, disabled if not (typically mobile).
  378. *
  379. * @sample {highstock} stock/rangeselector/input-datepicker/
  380. * Extending the input with a jQuery UI datepicker
  381. *
  382. * @type {boolean}
  383. * @default true
  384. * @apioption rangeSelector.inputEnabled
  385. */
  386. /**
  387. * Positioning for the input boxes. Allowed properties are `align`,
  388. * `x` and `y`.
  389. *
  390. * @since 1.2.4
  391. */
  392. inputPosition: {
  393. /**
  394. * The alignment of the input box. Allowed properties are `left`,
  395. * `center`, `right`.
  396. *
  397. * @sample {highstock} stock/rangeselector/input-button-position/
  398. * Alignment
  399. *
  400. * @type {Highcharts.AlignValue}
  401. * @since 6.0.0
  402. */
  403. align: 'right',
  404. /**
  405. * X offset of the input row.
  406. */
  407. x: 0,
  408. /**
  409. * Y offset of the input row.
  410. */
  411. y: 0
  412. },
  413. /**
  414. * The index of the button to appear pre-selected.
  415. *
  416. * @type {number}
  417. * @apioption rangeSelector.selected
  418. */
  419. /**
  420. * Positioning for the button row.
  421. *
  422. * @since 1.2.4
  423. */
  424. buttonPosition: {
  425. /**
  426. * The alignment of the input box. Allowed properties are `left`,
  427. * `center`, `right`.
  428. *
  429. * @sample {highstock} stock/rangeselector/input-button-position/
  430. * Alignment
  431. *
  432. * @type {Highcharts.AlignValue}
  433. * @since 6.0.0
  434. */
  435. align: 'left',
  436. /**
  437. * X offset of the button row.
  438. */
  439. x: 0,
  440. /**
  441. * Y offset of the button row.
  442. */
  443. y: 0
  444. },
  445. /**
  446. * CSS for the HTML inputs in the range selector.
  447. *
  448. * In styled mode, the inputs are styled by the
  449. * `.highcharts-range-input text` rule in SVG mode, and
  450. * `input.highcharts-range-selector` when active.
  451. *
  452. * @sample {highstock} stock/rangeselector/styling/
  453. * Styling the buttons and inputs
  454. *
  455. * @type {Highcharts.CSSObject}
  456. * @apioption rangeSelector.inputStyle
  457. */
  458. /**
  459. * CSS styles for the labels - the Zoom, From and To texts.
  460. *
  461. * In styled mode, the labels are styled by the
  462. * `.highcharts-range-label` class.
  463. *
  464. * @sample {highstock} stock/rangeselector/styling/
  465. * Styling the buttons and inputs
  466. *
  467. * @type {Highcharts.CSSObject}
  468. */
  469. labelStyle: {
  470. /** @ignore */
  471. color: '#666666'
  472. }
  473. }
  474. });
  475. defaultOptions.lang = merge(defaultOptions.lang,
  476. /**
  477. * Language object. The language object is global and it can't be set
  478. * on each chart initialization. Instead, use `Highcharts.setOptions` to
  479. * set it before any chart is initialized.
  480. *
  481. * ```js
  482. * Highcharts.setOptions({
  483. * lang: {
  484. * months: [
  485. * 'Janvier', 'Février', 'Mars', 'Avril',
  486. * 'Mai', 'Juin', 'Juillet', 'Août',
  487. * 'Septembre', 'Octobre', 'Novembre', 'Décembre'
  488. * ],
  489. * weekdays: [
  490. * 'Dimanche', 'Lundi', 'Mardi', 'Mercredi',
  491. * 'Jeudi', 'Vendredi', 'Samedi'
  492. * ]
  493. * }
  494. * });
  495. * ```
  496. *
  497. * @optionparent lang
  498. */
  499. {
  500. /**
  501. * The text for the label for the range selector buttons.
  502. *
  503. * @product highstock gantt
  504. */
  505. rangeSelectorZoom: 'Zoom',
  506. /**
  507. * The text for the label for the "from" input box in the range
  508. * selector.
  509. *
  510. * @product highstock gantt
  511. */
  512. rangeSelectorFrom: 'From',
  513. /**
  514. * The text for the label for the "to" input box in the range selector.
  515. *
  516. * @product highstock gantt
  517. */
  518. rangeSelectorTo: 'To'
  519. });
  520. /* eslint-disable no-invalid-this, valid-jsdoc */
  521. /**
  522. * The range selector.
  523. *
  524. * @private
  525. * @class
  526. * @name Highcharts.RangeSelector
  527. * @param {Highcharts.Chart} chart
  528. */
  529. var RangeSelector = /** @class */ (function () {
  530. function RangeSelector(chart) {
  531. /* *
  532. *
  533. * Properties
  534. *
  535. * */
  536. this.buttons = void 0;
  537. this.buttonOptions = RangeSelector.prototype.defaultButtons;
  538. this.options = void 0;
  539. this.chart = chart;
  540. // Run RangeSelector
  541. this.init(chart);
  542. }
  543. /**
  544. * The method to run when one of the buttons in the range selectors is
  545. * clicked
  546. *
  547. * @private
  548. * @function Highcharts.RangeSelector#clickButton
  549. * @param {number} i
  550. * The index of the button
  551. * @param {boolean} [redraw]
  552. * @return {void}
  553. */
  554. RangeSelector.prototype.clickButton = function (i, redraw) {
  555. 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
  556. type = rangeOptions.type, baseXAxisOptions, range = rangeOptions._range, rangeMin, minSetting, rangeSetting, ctx, ytdExtremes, dataGrouping = rangeOptions.dataGrouping;
  557. // chart has no data, base series is removed
  558. if (dataMin === null || dataMax === null) {
  559. return;
  560. }
  561. // Set the fixed range before range is altered
  562. chart.fixedRange = range;
  563. // Apply dataGrouping associated to button
  564. if (dataGrouping) {
  565. this.forcedDataGrouping = true;
  566. Axis.prototype.setDataGrouping.call(baseAxis || { chart: this.chart }, dataGrouping, false);
  567. this.frozenStates = rangeOptions.preserveDataGrouping;
  568. }
  569. // Apply range
  570. if (type === 'month' || type === 'year') {
  571. if (!baseAxis) {
  572. // This is set to the user options and picked up later when the
  573. // axis is instantiated so that we know the min and max.
  574. range = rangeOptions;
  575. }
  576. else {
  577. ctx = {
  578. range: rangeOptions,
  579. max: newMax,
  580. chart: chart,
  581. dataMin: dataMin,
  582. dataMax: dataMax
  583. };
  584. newMin = baseAxis.minFromRange.call(ctx);
  585. if (isNumber(ctx.newMax)) {
  586. newMax = ctx.newMax;
  587. }
  588. }
  589. // Fixed times like minutes, hours, days
  590. }
  591. else if (range) {
  592. newMin = Math.max(newMax - range, dataMin);
  593. newMax = Math.min(newMin + range, dataMax);
  594. }
  595. else if (type === 'ytd') {
  596. // On user clicks on the buttons, or a delayed action running from
  597. // the beforeRender event (below), the baseAxis is defined.
  598. if (baseAxis) {
  599. // When "ytd" is the pre-selected button for the initial view,
  600. // its calculation is delayed and rerun in the beforeRender
  601. // event (below). When the series are initialized, but before
  602. // the chart is rendered, we have access to the xData array
  603. // (#942).
  604. if (typeof dataMax === 'undefined') {
  605. dataMin = Number.MAX_VALUE;
  606. dataMax = Number.MIN_VALUE;
  607. chart.series.forEach(function (series) {
  608. // reassign it to the last item
  609. var xData = series.xData;
  610. dataMin = Math.min(xData[0], dataMin);
  611. dataMax = Math.max(xData[xData.length - 1], dataMax);
  612. });
  613. redraw = false;
  614. }
  615. ytdExtremes = rangeSelector.getYTDExtremes(dataMax, dataMin, chart.time.useUTC);
  616. newMin = rangeMin = ytdExtremes.min;
  617. newMax = ytdExtremes.max;
  618. // "ytd" is pre-selected. We don't yet have access to processed
  619. // point and extremes data (things like pointStart and pointInterval
  620. // are missing), so we delay the process (#942)
  621. }
  622. else {
  623. rangeSelector.deferredYTDClick = i;
  624. return;
  625. }
  626. }
  627. else if (type === 'all' && baseAxis) {
  628. newMin = dataMin;
  629. newMax = dataMax;
  630. }
  631. if (defined(newMin)) {
  632. newMin += rangeOptions._offsetMin;
  633. }
  634. if (defined(newMax)) {
  635. newMax += rangeOptions._offsetMax;
  636. }
  637. rangeSelector.setSelected(i);
  638. // Update the chart
  639. if (!baseAxis) {
  640. // Axis not yet instanciated. Temporarily set min and range
  641. // options and remove them on chart load (#4317).
  642. baseXAxisOptions = splat(chart.options.xAxis)[0];
  643. rangeSetting = baseXAxisOptions.range;
  644. baseXAxisOptions.range = range;
  645. minSetting = baseXAxisOptions.min;
  646. baseXAxisOptions.min = rangeMin;
  647. addEvent(chart, 'load', function resetMinAndRange() {
  648. baseXAxisOptions.range = rangeSetting;
  649. baseXAxisOptions.min = minSetting;
  650. });
  651. }
  652. else {
  653. // Existing axis object. Set extremes after render time.
  654. baseAxis.setExtremes(newMin, newMax, pick(redraw, 1), null, // auto animation
  655. {
  656. trigger: 'rangeSelectorButton',
  657. rangeSelectorButton: rangeOptions
  658. });
  659. }
  660. };
  661. /**
  662. * Set the selected option. This method only sets the internal flag, it
  663. * doesn't update the buttons or the actual zoomed range.
  664. *
  665. * @private
  666. * @function Highcharts.RangeSelector#setSelected
  667. * @param {number} [selected]
  668. * @return {void}
  669. */
  670. RangeSelector.prototype.setSelected = function (selected) {
  671. this.selected = this.options.selected = selected;
  672. };
  673. /**
  674. * Initialize the range selector
  675. *
  676. * @private
  677. * @function Highcharts.RangeSelector#init
  678. * @param {Highcharts.Chart} chart
  679. * @return {void}
  680. */
  681. RangeSelector.prototype.init = function (chart) {
  682. var rangeSelector = this, options = chart.options.rangeSelector, buttonOptions = options.buttons || rangeSelector.defaultButtons.slice(), selectedOption = options.selected, blurInputs = function () {
  683. var minInput = rangeSelector.minInput, maxInput = rangeSelector.maxInput;
  684. // #3274 in some case blur is not defined
  685. if (minInput && minInput.blur) {
  686. fireEvent(minInput, 'blur');
  687. }
  688. if (maxInput && maxInput.blur) {
  689. fireEvent(maxInput, 'blur');
  690. }
  691. };
  692. rangeSelector.chart = chart;
  693. rangeSelector.options = options;
  694. rangeSelector.buttons = [];
  695. rangeSelector.buttonOptions = buttonOptions;
  696. this.unMouseDown = addEvent(chart.container, 'mousedown', blurInputs);
  697. this.unResize = addEvent(chart, 'resize', blurInputs);
  698. // Extend the buttonOptions with actual range
  699. buttonOptions.forEach(rangeSelector.computeButtonRange);
  700. // zoomed range based on a pre-selected button index
  701. if (typeof selectedOption !== 'undefined' &&
  702. buttonOptions[selectedOption]) {
  703. this.clickButton(selectedOption, false);
  704. }
  705. addEvent(chart, 'load', function () {
  706. // If a data grouping is applied to the current button, release it
  707. // when extremes change
  708. if (chart.xAxis && chart.xAxis[0]) {
  709. addEvent(chart.xAxis[0], 'setExtremes', function (e) {
  710. if (this.max - this.min !==
  711. chart.fixedRange &&
  712. e.trigger !== 'rangeSelectorButton' &&
  713. e.trigger !== 'updatedData' &&
  714. rangeSelector.forcedDataGrouping &&
  715. !rangeSelector.frozenStates) {
  716. this.setDataGrouping(false, false);
  717. }
  718. });
  719. }
  720. });
  721. };
  722. /**
  723. * Dynamically update the range selector buttons after a new range has been
  724. * set
  725. *
  726. * @private
  727. * @function Highcharts.RangeSelector#updateButtonStates
  728. * @return {void}
  729. */
  730. RangeSelector.prototype.updateButtonStates = function () {
  731. 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
  732. unionExtremes = (chart.scroller &&
  733. 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;
  734. rangeSelector.buttonOptions.forEach(function (rangeOptions, i) {
  735. var range = rangeOptions._range, type = rangeOptions.type, count = rangeOptions.count || 1, button = buttons[i], state = 0, disable, select, offsetRange = rangeOptions._offsetMax -
  736. rangeOptions._offsetMin, isSelected = i === selected,
  737. // Disable buttons where the range exceeds what is allowed in
  738. // the current view
  739. isTooGreatRange = range >
  740. dataMax - dataMin,
  741. // Disable buttons where the range is smaller than the minimum
  742. // range
  743. isTooSmallRange = range < baseAxis.minRange,
  744. // Do not select the YTD button if not explicitly told so
  745. isYTDButNotSelected = false,
  746. // Disable the All button if we're already showing all
  747. isAllButAlreadyShowingAll = false, isSameRange = range === actualRange;
  748. // Months and years have a variable range so we check the extremes
  749. if ((type === 'month' || type === 'year') &&
  750. (actualRange + 36e5 >=
  751. { month: 28, year: 365 }[type] * day * count - offsetRange) &&
  752. (actualRange - 36e5 <=
  753. { month: 31, year: 366 }[type] * day * count + offsetRange)) {
  754. isSameRange = true;
  755. }
  756. else if (type === 'ytd') {
  757. isSameRange = (ytdMax - ytdMin + offsetRange) === actualRange;
  758. isYTDButNotSelected = !isSelected;
  759. }
  760. else if (type === 'all') {
  761. isSameRange = (baseAxis.max - baseAxis.min >=
  762. dataMax - dataMin);
  763. isAllButAlreadyShowingAll = (!isSelected &&
  764. selectedExists &&
  765. isSameRange);
  766. }
  767. // The new zoom area happens to match the range for a button - mark
  768. // it selected. This happens when scrolling across an ordinal gap.
  769. // It can be seen in the intraday demos when selecting 1h and scroll
  770. // across the night gap.
  771. disable = (!allButtonsEnabled &&
  772. (isTooGreatRange ||
  773. isTooSmallRange ||
  774. isAllButAlreadyShowingAll ||
  775. hasNoData));
  776. select = ((isSelected && isSameRange) ||
  777. (isSameRange && !selectedExists && !isYTDButNotSelected) ||
  778. (isSelected && rangeSelector.frozenStates));
  779. if (disable) {
  780. state = 3;
  781. }
  782. else if (select) {
  783. selectedExists = true; // Only one button can be selected
  784. state = 2;
  785. }
  786. // If state has changed, update the button
  787. if (button.state !== state) {
  788. button.setState(state);
  789. // Reset (#9209)
  790. if (state === 0 && selected === i) {
  791. rangeSelector.setSelected(null);
  792. }
  793. }
  794. });
  795. };
  796. /**
  797. * Compute and cache the range for an individual button
  798. *
  799. * @private
  800. * @function Highcharts.RangeSelector#computeButtonRange
  801. * @param {Highcharts.RangeSelectorButtonsOptions} rangeOptions
  802. * @return {void}
  803. */
  804. RangeSelector.prototype.computeButtonRange = function (rangeOptions) {
  805. var type = rangeOptions.type, count = rangeOptions.count || 1,
  806. // these time intervals have a fixed number of milliseconds, as
  807. // opposed to month, ytd and year
  808. fixedTimes = {
  809. millisecond: 1,
  810. second: 1000,
  811. minute: 60 * 1000,
  812. hour: 3600 * 1000,
  813. day: 24 * 3600 * 1000,
  814. week: 7 * 24 * 3600 * 1000
  815. };
  816. // Store the range on the button object
  817. if (fixedTimes[type]) {
  818. rangeOptions._range = fixedTimes[type] * count;
  819. }
  820. else if (type === 'month' || type === 'year') {
  821. rangeOptions._range = {
  822. month: 30,
  823. year: 365
  824. }[type] * 24 * 36e5 * count;
  825. }
  826. rangeOptions._offsetMin = pick(rangeOptions.offsetMin, 0);
  827. rangeOptions._offsetMax = pick(rangeOptions.offsetMax, 0);
  828. rangeOptions._range +=
  829. rangeOptions._offsetMax - rangeOptions._offsetMin;
  830. };
  831. /**
  832. * Set the internal and displayed value of a HTML input for the dates
  833. *
  834. * @private
  835. * @function Highcharts.RangeSelector#setInputValue
  836. * @param {string} name
  837. * @param {number} [inputTime]
  838. * @return {void}
  839. */
  840. RangeSelector.prototype.setInputValue = function (name, inputTime) {
  841. var options = this.chart.options.rangeSelector, time = this.chart.time, input = this[name + 'Input'];
  842. if (defined(inputTime)) {
  843. input.previousValue = input.HCTime;
  844. input.HCTime = inputTime;
  845. }
  846. input.value = time.dateFormat(options.inputEditDateFormat || '%Y-%m-%d', input.HCTime);
  847. this[name + 'DateBox'].attr({
  848. text: time.dateFormat(options.inputDateFormat || '%b %e, %Y', input.HCTime)
  849. });
  850. };
  851. /**
  852. * @private
  853. * @function Highcharts.RangeSelector#showInput
  854. * @param {string} name
  855. * @return {void}
  856. */
  857. RangeSelector.prototype.showInput = function (name) {
  858. var inputGroup = this.inputGroup, dateBox = this[name + 'DateBox'];
  859. css(this[name + 'Input'], {
  860. left: (inputGroup.translateX + dateBox.x) + 'px',
  861. top: inputGroup.translateY + 'px',
  862. width: (dateBox.width - 2) + 'px',
  863. height: (dateBox.height - 2) + 'px',
  864. border: '2px solid silver'
  865. });
  866. };
  867. /**
  868. * @private
  869. * @function Highcharts.RangeSelector#hideInput
  870. * @param {string} name
  871. * @return {void}
  872. */
  873. RangeSelector.prototype.hideInput = function (name) {
  874. css(this[name + 'Input'], {
  875. border: 0,
  876. width: '1px',
  877. height: '1px'
  878. });
  879. this.setInputValue(name);
  880. };
  881. /**
  882. * @private
  883. * @function Highcharts.RangeSelector#defaultInputDateParser
  884. */
  885. RangeSelector.prototype.defaultInputDateParser = function (inputDate, useUTC) {
  886. var date = new Date();
  887. if (H.isSafari) {
  888. return Date.parse(inputDate.split(' ').join('T'));
  889. }
  890. if (useUTC) {
  891. return Date.parse(inputDate + 'Z');
  892. }
  893. return Date.parse(inputDate) - date.getTimezoneOffset() * 60 * 1000;
  894. };
  895. /**
  896. * Draw either the 'from' or the 'to' HTML input box of the range selector
  897. *
  898. * @private
  899. * @function Highcharts.RangeSelector#drawInput
  900. * @param {string} name
  901. * @return {void}
  902. */
  903. RangeSelector.prototype.drawInput = function (name) {
  904. 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, defaultInputDateParser = this.defaultInputDateParser;
  905. /**
  906. * @private
  907. */
  908. function updateExtremes() {
  909. var inputValue = input.value, value, chartAxis = chart.xAxis[0], dataAxis = chart.scroller && chart.scroller.xAxis ?
  910. chart.scroller.xAxis :
  911. chartAxis, dataMin = dataAxis.dataMin, dataMax = dataAxis.dataMax;
  912. value = (options.inputDateParser || defaultInputDateParser)(inputValue, chart.time.useUTC);
  913. if (value !== input.previousValue) {
  914. input.previousValue = value;
  915. // If the value isn't parsed directly to a value by the
  916. // browser's Date.parse method, like YYYY-MM-DD in IE, try
  917. // parsing it a different way
  918. if (!isNumber(value)) {
  919. value = inputValue.split('-');
  920. value = Date.UTC(pInt(value[0]), pInt(value[1]) - 1, pInt(value[2]));
  921. }
  922. if (isNumber(value)) {
  923. // Correct for timezone offset (#433)
  924. if (!chart.time.useUTC) {
  925. value =
  926. value + new Date().getTimezoneOffset() * 60 * 1000;
  927. }
  928. // Validate the extremes. If it goes beyound the data min or
  929. // max, use the actual data extreme (#2438).
  930. if (isMin) {
  931. if (value > rangeSelector.maxInput.HCTime) {
  932. value = void 0;
  933. }
  934. else if (value < dataMin) {
  935. value = dataMin;
  936. }
  937. }
  938. else {
  939. if (value < rangeSelector.minInput.HCTime) {
  940. value = void 0;
  941. }
  942. else if (value > dataMax) {
  943. value = dataMax;
  944. }
  945. }
  946. // Set the extremes
  947. if (typeof value !== 'undefined') { // @todo typof undefined
  948. chartAxis.setExtremes(isMin ? value : chartAxis.min, isMin ? chartAxis.max : value, void 0, void 0, { trigger: 'rangeSelectorInput' });
  949. }
  950. }
  951. }
  952. }
  953. // Create the text label
  954. this[name + 'Label'] = label = renderer
  955. .label(lang[isMin ? 'rangeSelectorFrom' : 'rangeSelectorTo'], this.inputGroup.offset)
  956. .addClass('highcharts-range-label')
  957. .attr({
  958. padding: 2
  959. })
  960. .add(inputGroup);
  961. inputGroup.offset += label.width + 5;
  962. // Create an SVG label that shows updated date ranges and and records
  963. // click events that bring in the HTML input.
  964. this[name + 'DateBox'] = dateBox = renderer
  965. .label('', inputGroup.offset)
  966. .addClass('highcharts-range-input')
  967. .attr({
  968. padding: 2,
  969. width: options.inputBoxWidth || 90,
  970. height: options.inputBoxHeight || 17,
  971. 'text-align': 'center'
  972. })
  973. .on('click', function () {
  974. // If it is already focused, the onfocus event doesn't fire
  975. // (#3713)
  976. rangeSelector.showInput(name);
  977. rangeSelector[name + 'Input'].focus();
  978. });
  979. if (!chart.styledMode) {
  980. dateBox.attr({
  981. stroke: options.inputBoxBorderColor || '#cccccc',
  982. 'stroke-width': 1
  983. });
  984. }
  985. dateBox.add(inputGroup);
  986. inputGroup.offset += dateBox.width + (isMin ? 10 : 0);
  987. // Create the HTML input element. This is rendered as 1x1 pixel then set
  988. // to the right size when focused.
  989. this[name + 'Input'] = input = createElement('input', {
  990. name: name,
  991. className: 'highcharts-range-selector',
  992. type: 'text'
  993. }, {
  994. top: chart.plotTop + 'px' // prevent jump on focus in Firefox
  995. }, div);
  996. if (!chart.styledMode) {
  997. // Styles
  998. label.css(merge(chartStyle, options.labelStyle));
  999. dateBox.css(merge({
  1000. color: '#333333'
  1001. }, chartStyle, options.inputStyle));
  1002. css(input, extend({
  1003. position: 'absolute',
  1004. border: 0,
  1005. width: '1px',
  1006. height: '1px',
  1007. padding: 0,
  1008. textAlign: 'center',
  1009. fontSize: chartStyle.fontSize,
  1010. fontFamily: chartStyle.fontFamily,
  1011. top: '-9999em' // #4798
  1012. }, options.inputStyle));
  1013. }
  1014. // Blow up the input box
  1015. input.onfocus = function () {
  1016. rangeSelector.showInput(name);
  1017. };
  1018. // Hide away the input box
  1019. input.onblur = function () {
  1020. // update extermes only when inputs are active
  1021. if (input === H.doc.activeElement) { // Only when focused
  1022. // Update also when no `change` event is triggered, like when
  1023. // clicking inside the SVG (#4710)
  1024. updateExtremes();
  1025. }
  1026. // #10404 - move hide and blur outside focus
  1027. rangeSelector.hideInput(name);
  1028. input.blur(); // #4606
  1029. };
  1030. // handle changes in the input boxes
  1031. input.onchange = updateExtremes;
  1032. input.onkeypress = function (event) {
  1033. // IE does not fire onchange on enter
  1034. if (event.keyCode === 13) {
  1035. updateExtremes();
  1036. }
  1037. };
  1038. };
  1039. /**
  1040. * Get the position of the range selector buttons and inputs. This can be
  1041. * overridden from outside for custom positioning.
  1042. *
  1043. * @private
  1044. * @function Highcharts.RangeSelector#getPosition
  1045. *
  1046. * @return {Highcharts.Dictionary<number>}
  1047. */
  1048. RangeSelector.prototype.getPosition = function () {
  1049. var chart = this.chart, options = chart.options.rangeSelector, top = options.verticalAlign === 'top' ?
  1050. chart.plotTop - chart.axisOffset[0] :
  1051. 0; // set offset only for varticalAlign top
  1052. return {
  1053. buttonTop: top + options.buttonPosition.y,
  1054. inputTop: top + options.inputPosition.y - 10
  1055. };
  1056. };
  1057. /**
  1058. * Get the extremes of YTD. Will choose dataMax if its value is lower than
  1059. * the current timestamp. Will choose dataMin if its value is higher than
  1060. * the timestamp for the start of current year.
  1061. *
  1062. * @private
  1063. * @function Highcharts.RangeSelector#getYTDExtremes
  1064. *
  1065. * @param {number} dataMax
  1066. *
  1067. * @param {number} dataMin
  1068. *
  1069. * @return {*}
  1070. * Returns min and max for the YTD
  1071. */
  1072. RangeSelector.prototype.getYTDExtremes = function (dataMax, dataMin, useUTC) {
  1073. var time = this.chart.time, min, now = new time.Date(dataMax), year = time.get('FullYear', now), startOfYear = useUTC ?
  1074. time.Date.UTC(year, 0, 1) : // eslint-disable-line new-cap
  1075. +new time.Date(year, 0, 1);
  1076. min = Math.max(dataMin || 0, startOfYear);
  1077. now = now.getTime();
  1078. return {
  1079. max: Math.min(dataMax || now, now),
  1080. min: min
  1081. };
  1082. };
  1083. /**
  1084. * Render the range selector including the buttons and the inputs. The first
  1085. * time render is called, the elements are created and positioned. On
  1086. * subsequent calls, they are moved and updated.
  1087. *
  1088. * @private
  1089. * @function Highcharts.RangeSelector#render
  1090. * @param {number} [min]
  1091. * X axis minimum
  1092. * @param {number} [max]
  1093. * X axis maximum
  1094. * @return {void}
  1095. */
  1096. RangeSelector.prototype.render = function (min, max) {
  1097. var rangeSelector = this, chart = rangeSelector.chart, renderer = chart.renderer, container = chart.container, chartOptions = chart.options, navButtonOptions = (chartOptions.exporting &&
  1098. chartOptions.exporting.enabled !== false &&
  1099. chartOptions.navigation &&
  1100. chartOptions.navigation.buttonOptions), lang = defaultOptions.lang, div = rangeSelector.div, options = chartOptions.rangeSelector,
  1101. // Place inputs above the container
  1102. inputsZIndex = pick(chartOptions.chart.style &&
  1103. 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 = chart.hasLoaded, verb = animate ? 'animate' : 'attr', exportingX = 0, alignTranslateY, legendHeight, minPosition, translateY = 0, translateX;
  1104. if (options.enabled === false) {
  1105. return;
  1106. }
  1107. // create the elements
  1108. if (!rendered) {
  1109. rangeSelector.group = group = renderer.g('range-selector-group')
  1110. .attr({
  1111. zIndex: 7
  1112. })
  1113. .add();
  1114. rangeSelector.buttonGroup = buttonGroup =
  1115. renderer.g('range-selector-buttons').add(group);
  1116. rangeSelector.zoomText = renderer
  1117. .text(lang.rangeSelectorZoom, 0, 15)
  1118. .add(buttonGroup);
  1119. if (!chart.styledMode) {
  1120. rangeSelector.zoomText.css(options.labelStyle);
  1121. buttonTheme['stroke-width'] =
  1122. pick(buttonTheme['stroke-width'], 0);
  1123. }
  1124. rangeSelector.buttonOptions.forEach(function (rangeOptions, i) {
  1125. buttons[i] = renderer
  1126. .button(rangeOptions.text, 0, 0, function (e) {
  1127. // extract events from button object and call
  1128. var buttonEvents = (rangeOptions.events &&
  1129. rangeOptions.events.click), callDefaultEvent;
  1130. if (buttonEvents) {
  1131. callDefaultEvent =
  1132. buttonEvents.call(rangeOptions, e);
  1133. }
  1134. if (callDefaultEvent !== false) {
  1135. rangeSelector.clickButton(i);
  1136. }
  1137. rangeSelector.isActive = true;
  1138. }, buttonTheme, states && states.hover, states && states.select, states && states.disabled)
  1139. .attr({
  1140. 'text-align': 'center'
  1141. })
  1142. .add(buttonGroup);
  1143. });
  1144. // first create a wrapper outside the container in order to make
  1145. // the inputs work and make export correct
  1146. if (inputEnabled !== false) {
  1147. rangeSelector.div = div = createElement('div', null, {
  1148. position: 'relative',
  1149. height: 0,
  1150. zIndex: inputsZIndex
  1151. });
  1152. container.parentNode.insertBefore(div, container);
  1153. // Create the group to keep the inputs
  1154. rangeSelector.inputGroup = inputGroup =
  1155. renderer.g('input-group').add(group);
  1156. inputGroup.offset = 0;
  1157. rangeSelector.drawInput('min');
  1158. rangeSelector.drawInput('max');
  1159. }
  1160. }
  1161. // #8769, allow dynamically updating margins
  1162. rangeSelector.zoomText[verb]({
  1163. x: pick(plotLeft + buttonPosition.x, plotLeft)
  1164. });
  1165. // button start position
  1166. buttonLeft = pick(plotLeft + buttonPosition.x, plotLeft) +
  1167. rangeSelector.zoomText.getBBox().width + 5;
  1168. rangeSelector.buttonOptions.forEach(function (rangeOptions, i) {
  1169. buttons[i][verb]({ x: buttonLeft });
  1170. // increase button position for the next button
  1171. buttonLeft += buttons[i].width + pick(options.buttonSpacing, 5);
  1172. });
  1173. plotLeft = chart.plotLeft - chart.spacing[3];
  1174. rangeSelector.updateButtonStates();
  1175. // detect collisiton with exporting
  1176. if (navButtonOptions &&
  1177. this.titleCollision(chart) &&
  1178. verticalAlign === 'top' &&
  1179. buttonPosition.align === 'right' && ((buttonPosition.y +
  1180. buttonGroup.getBBox().height - 12) <
  1181. ((navButtonOptions.y || 0) +
  1182. navButtonOptions.height))) {
  1183. exportingX = -40;
  1184. }
  1185. translateX = buttonPosition.x - chart.spacing[3];
  1186. if (buttonPosition.align === 'right') {
  1187. translateX += exportingX - plotLeft; // (#13014)
  1188. }
  1189. else if (buttonPosition.align === 'center') {
  1190. translateX -= plotLeft / 2;
  1191. }
  1192. // align button group
  1193. buttonGroup.align({
  1194. y: buttonPosition.y,
  1195. width: buttonGroup.getBBox().width,
  1196. align: buttonPosition.align,
  1197. x: translateX
  1198. }, true, chart.spacingBox);
  1199. // skip animation
  1200. rangeSelector.group.placed = animate;
  1201. rangeSelector.buttonGroup.placed = animate;
  1202. if (inputEnabled !== false) {
  1203. var inputGroupX, inputGroupWidth, buttonGroupX, buttonGroupWidth;
  1204. // detect collision with exporting
  1205. if (navButtonOptions &&
  1206. this.titleCollision(chart) &&
  1207. verticalAlign === 'top' &&
  1208. inputPosition.align === 'right' && ((inputPosition.y -
  1209. inputGroup.getBBox().height - 12) <
  1210. ((navButtonOptions.y || 0) +
  1211. navButtonOptions.height +
  1212. chart.spacing[0]))) {
  1213. exportingX = -40;
  1214. }
  1215. else {
  1216. exportingX = 0;
  1217. }
  1218. if (inputPosition.align === 'left') {
  1219. translateX = plotLeft;
  1220. }
  1221. else if (inputPosition.align === 'right') {
  1222. translateX = -Math.max(chart.axisOffset[1], -exportingX);
  1223. }
  1224. // Update the alignment to the updated spacing box
  1225. inputGroup.align({
  1226. y: inputPosition.y,
  1227. width: inputGroup.getBBox().width,
  1228. align: inputPosition.align,
  1229. // fix wrong getBBox() value on right align
  1230. x: inputPosition.x + translateX - 2
  1231. }, true, chart.spacingBox);
  1232. // detect collision
  1233. inputGroupX = (inputGroup.alignAttr.translateX +
  1234. inputGroup.alignOptions.x -
  1235. exportingX +
  1236. // getBBox for detecing left margin
  1237. inputGroup.getBBox().x +
  1238. // 2px padding to not overlap input and label
  1239. 2);
  1240. inputGroupWidth = inputGroup.alignOptions.width;
  1241. buttonGroupX = buttonGroup.alignAttr.translateX +
  1242. buttonGroup.getBBox().x;
  1243. // 20 is minimal spacing between elements
  1244. buttonGroupWidth = buttonGroup.getBBox().width + 20;
  1245. if ((inputPosition.align ===
  1246. buttonPosition.align) || ((buttonGroupX + buttonGroupWidth > inputGroupX) &&
  1247. (inputGroupX + inputGroupWidth > buttonGroupX) &&
  1248. (buttonPositionY <
  1249. (inputPositionY +
  1250. inputGroup.getBBox().height)))) {
  1251. inputGroup.attr({
  1252. translateX: inputGroup.alignAttr.translateX +
  1253. (chart.axisOffset[1] >= -exportingX ? 0 : -exportingX),
  1254. translateY: inputGroup.alignAttr.translateY +
  1255. buttonGroup.getBBox().height + 10
  1256. });
  1257. }
  1258. // Set or reset the input values
  1259. rangeSelector.setInputValue('min', min);
  1260. rangeSelector.setInputValue('max', max);
  1261. // skip animation
  1262. rangeSelector.inputGroup.placed = animate;
  1263. }
  1264. // vertical align
  1265. rangeSelector.group.align({
  1266. verticalAlign: verticalAlign
  1267. }, true, chart.spacingBox);
  1268. // set position
  1269. groupHeight =
  1270. rangeSelector.group.getBBox().height + 20; // # 20 padding
  1271. alignTranslateY =
  1272. rangeSelector.group.alignAttr.translateY;
  1273. // calculate bottom position
  1274. if (verticalAlign === 'bottom') {
  1275. legendHeight = (legendOptions &&
  1276. legendOptions.verticalAlign === 'bottom' &&
  1277. legendOptions.enabled &&
  1278. !legendOptions.floating ?
  1279. legend.legendHeight + pick(legendOptions.margin, 10) :
  1280. 0);
  1281. groupHeight = groupHeight + legendHeight - 20;
  1282. translateY = (alignTranslateY -
  1283. groupHeight -
  1284. (floating ? 0 : options.y) -
  1285. (chart.titleOffset ? chart.titleOffset[2] : 0) -
  1286. 10 // 10 spacing
  1287. );
  1288. }
  1289. if (verticalAlign === 'top') {
  1290. if (floating) {
  1291. translateY = 0;
  1292. }
  1293. if (chart.titleOffset && chart.titleOffset[0]) {
  1294. translateY = chart.titleOffset[0];
  1295. }
  1296. translateY += ((chart.margin[0] - chart.spacing[0]) || 0);
  1297. }
  1298. else if (verticalAlign === 'middle') {
  1299. if (inputPositionY === buttonPositionY) {
  1300. if (inputPositionY < 0) {
  1301. translateY = alignTranslateY + minPosition;
  1302. }
  1303. else {
  1304. translateY = alignTranslateY;
  1305. }
  1306. }
  1307. else if (inputPositionY || buttonPositionY) {
  1308. if (inputPositionY < 0 ||
  1309. buttonPositionY < 0) {
  1310. translateY -= Math.min(inputPositionY, buttonPositionY);
  1311. }
  1312. else {
  1313. translateY =
  1314. alignTranslateY - groupHeight + minPosition;
  1315. }
  1316. }
  1317. }
  1318. rangeSelector.group.translate(options.x, options.y + Math.floor(translateY));
  1319. // translate HTML inputs
  1320. if (inputEnabled !== false) {
  1321. rangeSelector.minInput.style.marginTop =
  1322. rangeSelector.group.translateY + 'px';
  1323. rangeSelector.maxInput.style.marginTop =
  1324. rangeSelector.group.translateY + 'px';
  1325. }
  1326. rangeSelector.rendered = true;
  1327. };
  1328. /**
  1329. * Extracts height of range selector
  1330. *
  1331. * @private
  1332. * @function Highcharts.RangeSelector#getHeight
  1333. * @return {number}
  1334. * Returns rangeSelector height
  1335. */
  1336. RangeSelector.prototype.getHeight = function () {
  1337. 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;
  1338. if (options.height) {
  1339. return options.height;
  1340. }
  1341. rangeSelectorHeight = rangeSelectorGroup ?
  1342. // 13px to keep back compatibility
  1343. (rangeSelectorGroup.getBBox(true).height) + 13 +
  1344. yPosition :
  1345. 0;
  1346. minPosition = Math.min(inputPositionY, buttonPositionY);
  1347. if ((inputPositionY < 0 && buttonPositionY < 0) ||
  1348. (inputPositionY > 0 && buttonPositionY > 0)) {
  1349. rangeSelectorHeight += Math.abs(minPosition);
  1350. }
  1351. return rangeSelectorHeight;
  1352. };
  1353. /**
  1354. * Detect collision with title or subtitle
  1355. *
  1356. * @private
  1357. * @function Highcharts.RangeSelector#titleCollision
  1358. *
  1359. * @param {Highcharts.Chart} chart
  1360. *
  1361. * @return {boolean}
  1362. * Returns collision status
  1363. */
  1364. RangeSelector.prototype.titleCollision = function (chart) {
  1365. return !(chart.options.title.text ||
  1366. chart.options.subtitle.text);
  1367. };
  1368. /**
  1369. * Update the range selector with new options
  1370. *
  1371. * @private
  1372. * @function Highcharts.RangeSelector#update
  1373. * @param {Highcharts.RangeSelectorOptions} options
  1374. * @return {void}
  1375. */
  1376. RangeSelector.prototype.update = function (options) {
  1377. var chart = this.chart;
  1378. merge(true, chart.options.rangeSelector, options);
  1379. this.destroy();
  1380. this.init(chart);
  1381. chart.rangeSelector.render();
  1382. };
  1383. /**
  1384. * Destroys allocated elements.
  1385. *
  1386. * @private
  1387. * @function Highcharts.RangeSelector#destroy
  1388. */
  1389. RangeSelector.prototype.destroy = function () {
  1390. var rSelector = this, minInput = rSelector.minInput, maxInput = rSelector.maxInput;
  1391. rSelector.unMouseDown();
  1392. rSelector.unResize();
  1393. // Destroy elements in collections
  1394. destroyObjectProperties(rSelector.buttons);
  1395. // Clear input element events
  1396. if (minInput) {
  1397. minInput.onfocus = minInput.onblur = minInput.onchange = null;
  1398. }
  1399. if (maxInput) {
  1400. maxInput.onfocus = maxInput.onblur = maxInput.onchange = null;
  1401. }
  1402. // Destroy HTML and SVG elements
  1403. objectEach(rSelector, function (val, key) {
  1404. if (val && key !== 'chart') {
  1405. if (val instanceof SVGElement) {
  1406. // SVGElement
  1407. val.destroy();
  1408. }
  1409. else if (val instanceof window.HTMLElement) {
  1410. // HTML element
  1411. discardElement(val);
  1412. }
  1413. }
  1414. if (val !== RangeSelector.prototype[key]) {
  1415. rSelector[key] = null;
  1416. }
  1417. }, this);
  1418. };
  1419. return RangeSelector;
  1420. }());
  1421. /**
  1422. * The default buttons for pre-selecting time frames
  1423. */
  1424. RangeSelector.prototype.defaultButtons = [{
  1425. type: 'month',
  1426. count: 1,
  1427. text: '1m'
  1428. }, {
  1429. type: 'month',
  1430. count: 3,
  1431. text: '3m'
  1432. }, {
  1433. type: 'month',
  1434. count: 6,
  1435. text: '6m'
  1436. }, {
  1437. type: 'ytd',
  1438. text: 'YTD'
  1439. }, {
  1440. type: 'year',
  1441. count: 1,
  1442. text: '1y'
  1443. }, {
  1444. type: 'all',
  1445. text: 'All'
  1446. }];
  1447. /**
  1448. * Get the axis min value based on the range option and the current max. For
  1449. * stock charts this is extended via the {@link RangeSelector} so that if the
  1450. * selected range is a multiple of months or years, it is compensated for
  1451. * various month lengths.
  1452. *
  1453. * @private
  1454. * @function Highcharts.Axis#minFromRange
  1455. * @return {number|undefined}
  1456. * The new minimum value.
  1457. */
  1458. Axis.prototype.minFromRange = function () {
  1459. var rangeOptions = this.range, type = rangeOptions.type, min, max = this.max, dataMin, range, time = this.chart.time,
  1460. // Get the true range from a start date
  1461. getTrueRange = function (base, count) {
  1462. var timeName = type === 'year' ? 'FullYear' : 'Month';
  1463. var date = new time.Date(base);
  1464. var basePeriod = time.get(timeName, date);
  1465. time.set(timeName, date, basePeriod + count);
  1466. if (basePeriod === time.get(timeName, date)) {
  1467. time.set('Date', date, 0); // #6537
  1468. }
  1469. return date.getTime() - base;
  1470. };
  1471. if (isNumber(rangeOptions)) {
  1472. min = max - rangeOptions;
  1473. range = rangeOptions;
  1474. }
  1475. else {
  1476. min = max + getTrueRange(max, -rangeOptions.count);
  1477. // Let the fixedRange reflect initial settings (#5930)
  1478. if (this.chart) {
  1479. this.chart.fixedRange = max - min;
  1480. }
  1481. }
  1482. dataMin = pick(this.dataMin, Number.MIN_VALUE);
  1483. if (!isNumber(min)) {
  1484. min = dataMin;
  1485. }
  1486. if (min <= dataMin) {
  1487. min = dataMin;
  1488. if (typeof range === 'undefined') { // #4501
  1489. range = getTrueRange(min, rangeOptions.count);
  1490. }
  1491. this.newMax = Math.min(min + range, this.dataMax);
  1492. }
  1493. if (!isNumber(max)) {
  1494. min = void 0;
  1495. }
  1496. return min;
  1497. };
  1498. if (!H.RangeSelector) {
  1499. // Initialize rangeselector for stock charts
  1500. addEvent(Chart, 'afterGetContainer', function () {
  1501. if (this.options.rangeSelector.enabled) {
  1502. this.rangeSelector = new RangeSelector(this);
  1503. }
  1504. });
  1505. addEvent(Chart, 'beforeRender', function () {
  1506. var chart = this, axes = chart.axes, rangeSelector = chart.rangeSelector, verticalAlign;
  1507. if (rangeSelector) {
  1508. if (isNumber(rangeSelector.deferredYTDClick)) {
  1509. rangeSelector.clickButton(rangeSelector.deferredYTDClick);
  1510. delete rangeSelector.deferredYTDClick;
  1511. }
  1512. axes.forEach(function (axis) {
  1513. axis.updateNames();
  1514. axis.setScale();
  1515. });
  1516. chart.getAxisMargins();
  1517. rangeSelector.render();
  1518. verticalAlign = rangeSelector.options.verticalAlign;
  1519. if (!rangeSelector.options.floating) {
  1520. if (verticalAlign === 'bottom') {
  1521. this.extraBottomMargin = true;
  1522. }
  1523. else if (verticalAlign !== 'middle') {
  1524. this.extraTopMargin = true;
  1525. }
  1526. }
  1527. }
  1528. });
  1529. addEvent(Chart, 'update', function (e) {
  1530. var chart = this, options = e.options, optionsRangeSelector = options.rangeSelector, rangeSelector = chart.rangeSelector, verticalAlign, extraBottomMarginWas = this.extraBottomMargin, extraTopMarginWas = this.extraTopMargin;
  1531. if (optionsRangeSelector &&
  1532. optionsRangeSelector.enabled &&
  1533. !defined(rangeSelector)) {
  1534. this.options.rangeSelector.enabled = true;
  1535. this.rangeSelector = new RangeSelector(this);
  1536. }
  1537. this.extraBottomMargin = false;
  1538. this.extraTopMargin = false;
  1539. if (rangeSelector) {
  1540. rangeSelector.render();
  1541. verticalAlign = (optionsRangeSelector &&
  1542. optionsRangeSelector.verticalAlign) || (rangeSelector.options && rangeSelector.options.verticalAlign);
  1543. if (!rangeSelector.options.floating) {
  1544. if (verticalAlign === 'bottom') {
  1545. this.extraBottomMargin = true;
  1546. }
  1547. else if (verticalAlign !== 'middle') {
  1548. this.extraTopMargin = true;
  1549. }
  1550. }
  1551. if (this.extraBottomMargin !== extraBottomMarginWas ||
  1552. this.extraTopMargin !== extraTopMarginWas) {
  1553. this.isDirtyBox = true;
  1554. }
  1555. }
  1556. });
  1557. addEvent(Chart, 'render', function () {
  1558. var chart = this, rangeSelector = chart.rangeSelector, verticalAlign;
  1559. if (rangeSelector && !rangeSelector.options.floating) {
  1560. rangeSelector.render();
  1561. verticalAlign = rangeSelector.options.verticalAlign;
  1562. if (verticalAlign === 'bottom') {
  1563. this.extraBottomMargin = true;
  1564. }
  1565. else if (verticalAlign !== 'middle') {
  1566. this.extraTopMargin = true;
  1567. }
  1568. }
  1569. });
  1570. addEvent(Chart, 'getMargins', function () {
  1571. var rangeSelector = this.rangeSelector, rangeSelectorHeight;
  1572. if (rangeSelector) {
  1573. rangeSelectorHeight = rangeSelector.getHeight();
  1574. if (this.extraTopMargin) {
  1575. this.plotTop += rangeSelectorHeight;
  1576. }
  1577. if (this.extraBottomMargin) {
  1578. this.marginBottom += rangeSelectorHeight;
  1579. }
  1580. }
  1581. });
  1582. Chart.prototype.callbacks.push(function (chart) {
  1583. var extremes, rangeSelector = chart.rangeSelector, unbindRender, unbindSetExtremes, legend, alignTo, verticalAlign;
  1584. /**
  1585. * @private
  1586. */
  1587. function renderRangeSelector() {
  1588. extremes = chart.xAxis[0].getExtremes();
  1589. legend = chart.legend;
  1590. verticalAlign = rangeSelector === null || rangeSelector === void 0 ? void 0 : rangeSelector.options.verticalAlign;
  1591. if (isNumber(extremes.min)) {
  1592. rangeSelector.render(extremes.min, extremes.max);
  1593. }
  1594. // Re-align the legend so that it's below the rangeselector
  1595. if (rangeSelector && legend.display &&
  1596. verticalAlign === 'top' &&
  1597. verticalAlign === legend.options.verticalAlign) {
  1598. // Create a new alignment box for the legend.
  1599. alignTo = merge(chart.spacingBox);
  1600. if (legend.options.layout === 'vertical') {
  1601. alignTo.y = chart.plotTop;
  1602. }
  1603. else {
  1604. alignTo.y += rangeSelector.getHeight();
  1605. }
  1606. legend.group.placed = false; // Don't animate the alignment.
  1607. legend.align(alignTo);
  1608. }
  1609. }
  1610. if (rangeSelector) {
  1611. // redraw the scroller on setExtremes
  1612. unbindSetExtremes = addEvent(chart.xAxis[0], 'afterSetExtremes', function (e) {
  1613. rangeSelector.render(e.min, e.max);
  1614. });
  1615. // redraw the scroller chart resize
  1616. unbindRender = addEvent(chart, 'redraw', renderRangeSelector);
  1617. // do it now
  1618. renderRangeSelector();
  1619. }
  1620. // Remove resize/afterSetExtremes at chart destroy
  1621. addEvent(chart, 'destroy', function destroyEvents() {
  1622. if (rangeSelector) {
  1623. unbindRender();
  1624. unbindSetExtremes();
  1625. }
  1626. });
  1627. });
  1628. H.RangeSelector = RangeSelector;
  1629. }
  1630. export default H.RangeSelector;