RangeSelector.js 64 KB

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