RangeSelector.js 60 KB

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