Tooltip.js 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332
  1. /* *
  2. *
  3. * (c) 2010-2020 Torstein Honsi
  4. *
  5. * License: www.highcharts.com/license
  6. *
  7. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  8. *
  9. * */
  10. 'use strict';
  11. import H from './Globals.js';
  12. var doc = H.doc;
  13. import U from './Utilities.js';
  14. var clamp = U.clamp, css = U.css, defined = U.defined, discardElement = U.discardElement, extend = U.extend, fireEvent = U.fireEvent, format = U.format, isNumber = U.isNumber, isString = U.isString, merge = U.merge, pick = U.pick, splat = U.splat, syncTimeout = U.syncTimeout, timeUnits = U.timeUnits;
  15. /**
  16. * Callback function to format the text of the tooltip from scratch.
  17. *
  18. * In case of single or shared tooltips, a string should be be returned. In case
  19. * of splitted tooltips, it should return an array where the first item is the
  20. * header, and subsequent items are mapped to the points. Return `false` to
  21. * disable tooltip for a specific point on series.
  22. *
  23. * @callback Highcharts.TooltipFormatterCallbackFunction
  24. *
  25. * @param {Highcharts.TooltipFormatterContextObject} this
  26. * Context to format
  27. *
  28. * @param {Highcharts.Tooltip} tooltip
  29. * The tooltip instance
  30. *
  31. * @return {false|string|Array<(string|null|undefined)>|null|undefined}
  32. * Formatted text or false
  33. */
  34. /**
  35. * @interface Highcharts.TooltipFormatterContextObject
  36. */ /**
  37. * @name Highcharts.TooltipFormatterContextObject#color
  38. * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
  39. */ /**
  40. * @name Highcharts.TooltipFormatterContextObject#colorIndex
  41. * @type {number|undefined}
  42. */ /**
  43. * @name Highcharts.TooltipFormatterContextObject#key
  44. * @type {number}
  45. */ /**
  46. * @name Highcharts.TooltipFormatterContextObject#percentage
  47. * @type {number|undefined}
  48. */ /**
  49. * @name Highcharts.TooltipFormatterContextObject#point
  50. * @type {Highcharts.Point}
  51. */ /**
  52. * @name Highcharts.TooltipFormatterContextObject#points
  53. * @type {Array<Highcharts.TooltipFormatterContextObject>|undefined}
  54. */ /**
  55. * @name Highcharts.TooltipFormatterContextObject#series
  56. * @type {Highcharts.Series}
  57. */ /**
  58. * @name Highcharts.TooltipFormatterContextObject#total
  59. * @type {number|undefined}
  60. */ /**
  61. * @name Highcharts.TooltipFormatterContextObject#x
  62. * @type {number}
  63. */ /**
  64. * @name Highcharts.TooltipFormatterContextObject#y
  65. * @type {number}
  66. */
  67. /**
  68. * A callback function to place the tooltip in a specific position.
  69. *
  70. * @callback Highcharts.TooltipPositionerCallbackFunction
  71. *
  72. * @param {Highcharts.Tooltip} this
  73. * Tooltip context of the callback.
  74. *
  75. * @param {number} labelWidth
  76. * Width of the tooltip.
  77. *
  78. * @param {number} labelHeight
  79. * Height of the tooltip.
  80. *
  81. * @param {Highcharts.Point|Highcharts.TooltipPositionerPointObject} point
  82. * Point information for positioning a tooltip.
  83. *
  84. * @return {Highcharts.PositionObject}
  85. * New position for the tooltip.
  86. */
  87. /**
  88. * Point information for positioning a tooltip.
  89. *
  90. * @interface Highcharts.TooltipPositionerPointObject
  91. */ /**
  92. * If `tooltip.split` option is enabled and positioner is called for each of the
  93. * boxes separately, this property indicates the call on the xAxis header, which
  94. * is not a point itself.
  95. * @name Highcharts.TooltipPositionerPointObject#isHeader
  96. * @type {boolean}
  97. */ /**
  98. * The reference point relative to the plot area. Add chart.plotLeft to get the
  99. * full coordinates.
  100. * @name Highcharts.TooltipPositionerPointObject#plotX
  101. * @type {number}
  102. */ /**
  103. * The reference point relative to the plot area. Add chart.plotTop to get the
  104. * full coordinates.
  105. * @name Highcharts.TooltipPositionerPointObject#plotY
  106. * @type {number}
  107. */
  108. /**
  109. * @typedef {"callout"|"circle"|"square"} Highcharts.TooltipShapeValue
  110. */
  111. ''; // separates doclets above from variables below
  112. /* eslint-disable no-invalid-this, valid-jsdoc */
  113. /**
  114. * Tooltip of a chart.
  115. *
  116. * @class
  117. * @name Highcharts.Tooltip
  118. *
  119. * @param {Highcharts.Chart} chart
  120. * The chart instance.
  121. *
  122. * @param {Highcharts.TooltipOptions} options
  123. * Tooltip options.
  124. */
  125. var Tooltip = /** @class */ (function () {
  126. /* *
  127. *
  128. * Constructors
  129. *
  130. * */
  131. function Tooltip(chart, options) {
  132. this.container = void 0;
  133. this.crosshairs = [];
  134. this.distance = 0;
  135. this.isHidden = true;
  136. this.isSticky = false;
  137. this.now = {};
  138. this.options = {};
  139. this.outside = false;
  140. this.chart = chart;
  141. this.init(chart, options);
  142. }
  143. /* *
  144. *
  145. * Functions
  146. *
  147. * */
  148. /**
  149. * In styled mode, apply the default filter for the tooltip drop-shadow. It
  150. * needs to have an id specific to the chart, otherwise there will be issues
  151. * when one tooltip adopts the filter of a different chart, specifically one
  152. * where the container is hidden.
  153. *
  154. * @private
  155. * @function Highcharts.Tooltip#applyFilter
  156. */
  157. Tooltip.prototype.applyFilter = function () {
  158. var chart = this.chart;
  159. chart.renderer.definition({
  160. tagName: 'filter',
  161. id: 'drop-shadow-' + chart.index,
  162. opacity: 0.5,
  163. children: [{
  164. tagName: 'feGaussianBlur',
  165. 'in': 'SourceAlpha',
  166. stdDeviation: 1
  167. }, {
  168. tagName: 'feOffset',
  169. dx: 1,
  170. dy: 1
  171. }, {
  172. tagName: 'feComponentTransfer',
  173. children: [{
  174. tagName: 'feFuncA',
  175. type: 'linear',
  176. slope: 0.3
  177. }]
  178. }, {
  179. tagName: 'feMerge',
  180. children: [{
  181. tagName: 'feMergeNode'
  182. }, {
  183. tagName: 'feMergeNode',
  184. 'in': 'SourceGraphic'
  185. }]
  186. }]
  187. });
  188. chart.renderer.definition({
  189. tagName: 'style',
  190. textContent: '.highcharts-tooltip-' + chart.index + '{' +
  191. 'filter:url(#drop-shadow-' + chart.index + ')' +
  192. '}'
  193. });
  194. };
  195. /**
  196. * Build the body (lines) of the tooltip by iterating over the items and
  197. * returning one entry for each item, abstracting this functionality allows
  198. * to easily overwrite and extend it.
  199. *
  200. * @private
  201. * @function Highcharts.Tooltip#bodyFormatter
  202. * @param {Array<(Highcharts.Point|Highcharts.Series)>} items
  203. * @return {Array<string>}
  204. */
  205. Tooltip.prototype.bodyFormatter = function (items) {
  206. return items.map(function (item) {
  207. var tooltipOptions = item.series.tooltipOptions;
  208. return (tooltipOptions[(item.point.formatPrefix || 'point') + 'Formatter'] ||
  209. item.point.tooltipFormatter).call(item.point, tooltipOptions[(item.point.formatPrefix || 'point') + 'Format'] || '');
  210. });
  211. };
  212. /**
  213. * Destroy the single tooltips in a split tooltip.
  214. * If the tooltip is active then it is not destroyed, unless forced to.
  215. *
  216. * @private
  217. * @function Highcharts.Tooltip#cleanSplit
  218. *
  219. * @param {boolean} [force]
  220. * Force destroy all tooltips.
  221. */
  222. Tooltip.prototype.cleanSplit = function (force) {
  223. this.chart.series.forEach(function (series) {
  224. var tt = series && series.tt;
  225. if (tt) {
  226. if (!tt.isActive || force) {
  227. series.tt = tt.destroy();
  228. }
  229. else {
  230. tt.isActive = false;
  231. }
  232. }
  233. });
  234. };
  235. /**
  236. * In case no user defined formatter is given, this will be used. Note that
  237. * the context here is an object holding point, series, x, y etc.
  238. *
  239. * @function Highcharts.Tooltip#defaultFormatter
  240. *
  241. * @param {Highcharts.Tooltip} tooltip
  242. *
  243. * @return {Array<string>}
  244. */
  245. Tooltip.prototype.defaultFormatter = function (tooltip) {
  246. var items = this.points || splat(this), s;
  247. // Build the header
  248. s = [tooltip.tooltipFooterHeaderFormatter(items[0])];
  249. // build the values
  250. s = s.concat(tooltip.bodyFormatter(items));
  251. // footer
  252. s.push(tooltip.tooltipFooterHeaderFormatter(items[0], true));
  253. return s;
  254. };
  255. /**
  256. * Removes and destroys the tooltip and its elements.
  257. *
  258. * @function Highcharts.Tooltip#destroy
  259. */
  260. Tooltip.prototype.destroy = function () {
  261. // Destroy and clear local variables
  262. if (this.label) {
  263. this.label = this.label.destroy();
  264. }
  265. if (this.split && this.tt) {
  266. this.cleanSplit(this.chart, true);
  267. this.tt = this.tt.destroy();
  268. }
  269. if (this.renderer) {
  270. this.renderer = this.renderer.destroy();
  271. discardElement(this.container);
  272. }
  273. U.clearTimeout(this.hideTimer);
  274. U.clearTimeout(this.tooltipTimeout);
  275. };
  276. /**
  277. * Extendable method to get the anchor position of the tooltip
  278. * from a point or set of points
  279. *
  280. * @private
  281. * @function Highcharts.Tooltip#getAnchor
  282. *
  283. * @param {Highcharts.Point|Array<Highcharts.Point>} points
  284. *
  285. * @param {Highcharts.PointerEventObject} [mouseEvent]
  286. *
  287. * @return {Array<number>}
  288. */
  289. Tooltip.prototype.getAnchor = function (points, mouseEvent) {
  290. var ret, chart = this.chart, pointer = chart.pointer, inverted = chart.inverted, plotTop = chart.plotTop, plotLeft = chart.plotLeft, plotX = 0, plotY = 0, yAxis, xAxis;
  291. points = splat(points);
  292. // When tooltip follows mouse, relate the position to the mouse
  293. if (this.followPointer && mouseEvent) {
  294. if (typeof mouseEvent.chartX === 'undefined') {
  295. mouseEvent = pointer.normalize(mouseEvent);
  296. }
  297. ret = [
  298. mouseEvent.chartX - plotLeft,
  299. mouseEvent.chartY - plotTop
  300. ];
  301. // Some series types use a specificly calculated tooltip position for
  302. // each point
  303. }
  304. else if (points[0].tooltipPos) {
  305. ret = points[0].tooltipPos;
  306. // When shared, use the average position
  307. }
  308. else {
  309. points.forEach(function (point) {
  310. yAxis = point.series.yAxis;
  311. xAxis = point.series.xAxis;
  312. plotX += point.plotX +
  313. (!inverted && xAxis ? xAxis.left - plotLeft : 0);
  314. plotY += (point.plotLow ?
  315. (point.plotLow + point.plotHigh) / 2 :
  316. point.plotY) + (!inverted && yAxis ? yAxis.top - plotTop : 0); // #1151
  317. });
  318. plotX /= points.length;
  319. plotY /= points.length;
  320. ret = [
  321. inverted ? chart.plotWidth - plotY : plotX,
  322. this.shared && !inverted && points.length > 1 && mouseEvent ?
  323. // place shared tooltip next to the mouse (#424)
  324. mouseEvent.chartY - plotTop :
  325. inverted ? chart.plotHeight - plotX : plotY
  326. ];
  327. }
  328. return ret.map(Math.round);
  329. };
  330. /**
  331. * Get the optimal date format for a point, based on a range.
  332. *
  333. * @private
  334. * @function Highcharts.Tooltip#getDateFormat
  335. *
  336. * @param {number} range
  337. * The time range
  338. *
  339. * @param {number} date
  340. * The date of the point in question
  341. *
  342. * @param {number} startOfWeek
  343. * An integer representing the first day of the week, where 0 is
  344. * Sunday.
  345. *
  346. * @param {Highcharts.Dictionary<string>} dateTimeLabelFormats
  347. * A map of time units to formats.
  348. *
  349. * @return {string}
  350. * The optimal date format for a point.
  351. */
  352. Tooltip.prototype.getDateFormat = function (range, date, startOfWeek, dateTimeLabelFormats) {
  353. var time = this.chart.time, dateStr = time.dateFormat('%m-%d %H:%M:%S.%L', date), format, n, blank = '01-01 00:00:00.000', strpos = {
  354. millisecond: 15,
  355. second: 12,
  356. minute: 9,
  357. hour: 6,
  358. day: 3
  359. }, lastN = 'millisecond'; // for sub-millisecond data, #4223
  360. for (n in timeUnits) { // eslint-disable-line guard-for-in
  361. // If the range is exactly one week and we're looking at a
  362. // Sunday/Monday, go for the week format
  363. if (range === timeUnits.week &&
  364. +time.dateFormat('%w', date) === startOfWeek &&
  365. dateStr.substr(6) === blank.substr(6)) {
  366. n = 'week';
  367. break;
  368. }
  369. // The first format that is too great for the range
  370. if (timeUnits[n] > range) {
  371. n = lastN;
  372. break;
  373. }
  374. // If the point is placed every day at 23:59, we need to show
  375. // the minutes as well. #2637.
  376. if (strpos[n] &&
  377. dateStr.substr(strpos[n]) !== blank.substr(strpos[n])) {
  378. break;
  379. }
  380. // Weeks are outside the hierarchy, only apply them on
  381. // Mondays/Sundays like in the first condition
  382. if (n !== 'week') {
  383. lastN = n;
  384. }
  385. }
  386. if (n) {
  387. format = time.resolveDTLFormat(dateTimeLabelFormats[n]).main;
  388. }
  389. return format;
  390. };
  391. /**
  392. * Creates the Tooltip label element if it does not exist, then returns it.
  393. *
  394. * @function Highcharts.Tooltip#getLabel
  395. * @return {Highcharts.SVGElement}
  396. */
  397. Tooltip.prototype.getLabel = function () {
  398. var _a, _b;
  399. var tooltip = this, renderer = this.chart.renderer, styledMode = this.chart.styledMode, options = this.options, className = ('tooltip' + (defined(options.className) ?
  400. ' ' + options.className :
  401. '')), pointerEvents = (((_a = options.style) === null || _a === void 0 ? void 0 : _a.pointerEvents) ||
  402. (!this.followPointer && options.stickOnContact ? 'auto' : 'none')), container, set, onMouseEnter = function () {
  403. tooltip.inContact = true;
  404. }, onMouseLeave = function () {
  405. var series = tooltip.chart.hoverSeries;
  406. tooltip.inContact = false;
  407. if (series &&
  408. series.onMouseOut) {
  409. series.onMouseOut();
  410. }
  411. };
  412. if (!this.label) {
  413. if (this.outside) {
  414. /**
  415. * Reference to the tooltip's container, when
  416. * [Highcharts.Tooltip#outside] is set to true, otherwise
  417. * it's undefined.
  418. *
  419. * @name Highcharts.Tooltip#container
  420. * @type {Highcharts.HTMLDOMElement|undefined}
  421. */
  422. this.container = container = H.doc.createElement('div');
  423. container.className = 'highcharts-tooltip-container';
  424. css(container, {
  425. position: 'absolute',
  426. top: '1px',
  427. pointerEvents: pointerEvents,
  428. zIndex: 3
  429. });
  430. H.doc.body.appendChild(container);
  431. /**
  432. * Reference to the tooltip's renderer, when
  433. * [Highcharts.Tooltip#outside] is set to true, otherwise
  434. * it's undefined.
  435. *
  436. * @name Highcharts.Tooltip#renderer
  437. * @type {Highcharts.SVGRenderer|undefined}
  438. */
  439. this.renderer = renderer = new H.Renderer(container, 0, 0, (_b = this.chart.options.chart) === null || _b === void 0 ? void 0 : _b.style, void 0, void 0, renderer.styledMode);
  440. }
  441. // Create the label
  442. if (this.split) {
  443. this.label = renderer.g(className);
  444. }
  445. else {
  446. this.label = renderer
  447. .label('', 0, 0, options.shape || 'callout', null, null, options.useHTML, null, className)
  448. .attr({
  449. padding: options.padding,
  450. r: options.borderRadius
  451. });
  452. if (!styledMode) {
  453. this.label
  454. .attr({
  455. fill: options.backgroundColor,
  456. 'stroke-width': options.borderWidth
  457. })
  458. // #2301, #2657
  459. .css(options.style)
  460. .css({ pointerEvents: pointerEvents })
  461. .shadow(options.shadow);
  462. }
  463. }
  464. if (styledMode) {
  465. // Apply the drop-shadow filter
  466. this.applyFilter();
  467. this.label.addClass('highcharts-tooltip-' + this.chart.index);
  468. }
  469. // Split tooltip use updateTooltipContainer to position the tooltip
  470. // container.
  471. if (tooltip.outside && !tooltip.split) {
  472. var label_1 = this.label;
  473. var xSetter_1 = label_1.xSetter, ySetter_1 = label_1.ySetter;
  474. label_1.xSetter = function (value) {
  475. xSetter_1.call(label_1, tooltip.distance);
  476. container.style.left = value + 'px';
  477. };
  478. label_1.ySetter = function (value) {
  479. ySetter_1.call(label_1, tooltip.distance);
  480. container.style.top = value + 'px';
  481. };
  482. }
  483. this.label
  484. .on('mouseenter', onMouseEnter)
  485. .on('mouseleave', onMouseLeave)
  486. .attr({ zIndex: 8 })
  487. .add();
  488. }
  489. return this.label;
  490. };
  491. /**
  492. * Place the tooltip in a chart without spilling over
  493. * and not covering the point it self.
  494. *
  495. * @private
  496. * @function Highcharts.Tooltip#getPosition
  497. *
  498. * @param {number} boxWidth
  499. *
  500. * @param {number} boxHeight
  501. *
  502. * @param {Highcharts.Point} point
  503. *
  504. * @return {Highcharts.PositionObject}
  505. */
  506. Tooltip.prototype.getPosition = function (boxWidth, boxHeight, point) {
  507. var chart = this.chart, distance = this.distance, ret = {},
  508. // Don't use h if chart isn't inverted (#7242) ???
  509. h = (chart.inverted && point.h) || 0, // #4117 ???
  510. swapped, outside = this.outside, outerWidth = outside ?
  511. // substract distance to prevent scrollbars
  512. doc.documentElement.clientWidth - 2 * distance :
  513. chart.chartWidth, outerHeight = outside ?
  514. Math.max(doc.body.scrollHeight, doc.documentElement.scrollHeight, doc.body.offsetHeight, doc.documentElement.offsetHeight, doc.documentElement.clientHeight) :
  515. chart.chartHeight, chartPosition = chart.pointer.getChartPosition(), containerScaling = chart.containerScaling, scaleX = function (val) { return ( // eslint-disable-line no-confusing-arrow
  516. containerScaling ? val * containerScaling.scaleX : val); }, scaleY = function (val) { return ( // eslint-disable-line no-confusing-arrow
  517. containerScaling ? val * containerScaling.scaleY : val); },
  518. // Build parameter arrays for firstDimension()/secondDimension()
  519. buildDimensionArray = function (dim) {
  520. var isX = dim === 'x';
  521. return [
  522. dim,
  523. isX ? outerWidth : outerHeight,
  524. isX ? boxWidth : boxHeight
  525. ].concat(outside ? [
  526. // If we are using tooltip.outside, we need to scale the
  527. // position to match scaling of the container in case there
  528. // is a transform/zoom on the container. #11329
  529. isX ? scaleX(boxWidth) : scaleY(boxHeight),
  530. isX ? chartPosition.left - distance +
  531. scaleX(point.plotX + chart.plotLeft) :
  532. chartPosition.top - distance +
  533. scaleY(point.plotY + chart.plotTop),
  534. 0,
  535. isX ? outerWidth : outerHeight
  536. ] : [
  537. // Not outside, no scaling is needed
  538. isX ? boxWidth : boxHeight,
  539. isX ? point.plotX + chart.plotLeft :
  540. point.plotY + chart.plotTop,
  541. isX ? chart.plotLeft : chart.plotTop,
  542. isX ? chart.plotLeft + chart.plotWidth :
  543. chart.plotTop + chart.plotHeight
  544. ]);
  545. }, first = buildDimensionArray('y'), second = buildDimensionArray('x'),
  546. // The far side is right or bottom
  547. preferFarSide = !this.followPointer && pick(point.ttBelow, !chart.inverted === !!point.negative), // #4984
  548. /*
  549. * Handle the preferred dimension. When the preferred dimension is
  550. * tooltip on top or bottom of the point, it will look for space
  551. * there.
  552. *
  553. * @private
  554. */
  555. firstDimension = function (dim, outerSize, innerSize, scaledInnerSize, // #11329
  556. point, min, max) {
  557. var scaledDist = dim === 'y' ?
  558. scaleY(distance) : scaleX(distance), scaleDiff = (innerSize - scaledInnerSize) / 2, roomLeft = scaledInnerSize < point - distance, roomRight = point + distance + scaledInnerSize < outerSize, alignedLeft = point - scaledDist - innerSize + scaleDiff, alignedRight = point + scaledDist - scaleDiff;
  559. if (preferFarSide && roomRight) {
  560. ret[dim] = alignedRight;
  561. }
  562. else if (!preferFarSide && roomLeft) {
  563. ret[dim] = alignedLeft;
  564. }
  565. else if (roomLeft) {
  566. ret[dim] = Math.min(max - scaledInnerSize, alignedLeft - h < 0 ? alignedLeft : alignedLeft - h);
  567. }
  568. else if (roomRight) {
  569. ret[dim] = Math.max(min, alignedRight + h + innerSize > outerSize ?
  570. alignedRight :
  571. alignedRight + h);
  572. }
  573. else {
  574. return false;
  575. }
  576. },
  577. /*
  578. * Handle the secondary dimension. If the preferred dimension is
  579. * tooltip on top or bottom of the point, the second dimension is to
  580. * align the tooltip above the point, trying to align center but
  581. * allowing left or right align within the chart box.
  582. *
  583. * @private
  584. */
  585. secondDimension = function (dim, outerSize, innerSize, scaledInnerSize, // #11329
  586. point) {
  587. var retVal;
  588. // Too close to the edge, return false and swap dimensions
  589. if (point < distance || point > outerSize - distance) {
  590. retVal = false;
  591. // Align left/top
  592. }
  593. else if (point < innerSize / 2) {
  594. ret[dim] = 1;
  595. // Align right/bottom
  596. }
  597. else if (point > outerSize - scaledInnerSize / 2) {
  598. ret[dim] = outerSize - scaledInnerSize - 2;
  599. // Align center
  600. }
  601. else {
  602. ret[dim] = point - innerSize / 2;
  603. }
  604. return retVal;
  605. },
  606. /*
  607. * Swap the dimensions
  608. */
  609. swap = function (count) {
  610. var temp = first;
  611. first = second;
  612. second = temp;
  613. swapped = count;
  614. }, run = function () {
  615. if (firstDimension.apply(0, first) !== false) {
  616. if (secondDimension.apply(0, second) === false &&
  617. !swapped) {
  618. swap(true);
  619. run();
  620. }
  621. }
  622. else if (!swapped) {
  623. swap(true);
  624. run();
  625. }
  626. else {
  627. ret.x = ret.y = 0;
  628. }
  629. };
  630. // Under these conditions, prefer the tooltip on the side of the point
  631. if (chart.inverted || this.len > 1) {
  632. swap();
  633. }
  634. run();
  635. return ret;
  636. };
  637. /**
  638. * Get the best X date format based on the closest point range on the axis.
  639. *
  640. * @private
  641. * @function Highcharts.Tooltip#getXDateFormat
  642. *
  643. * @param {Highcharts.Point} point
  644. *
  645. * @param {Highcharts.TooltipOptions} options
  646. *
  647. * @param {Highcharts.Axis} xAxis
  648. *
  649. * @return {string}
  650. */
  651. Tooltip.prototype.getXDateFormat = function (point, options, xAxis) {
  652. var xDateFormat, dateTimeLabelFormats = options.dateTimeLabelFormats, closestPointRange = xAxis && xAxis.closestPointRange;
  653. if (closestPointRange) {
  654. xDateFormat = this.getDateFormat(closestPointRange, point.x, xAxis.options.startOfWeek, dateTimeLabelFormats);
  655. }
  656. else {
  657. xDateFormat = dateTimeLabelFormats.day;
  658. }
  659. return xDateFormat || dateTimeLabelFormats.year; // #2546, 2581
  660. };
  661. /**
  662. * Hides the tooltip with a fade out animation.
  663. *
  664. * @function Highcharts.Tooltip#hide
  665. *
  666. * @param {number} [delay]
  667. * The fade out in milliseconds. If no value is provided the value
  668. * of the tooltip.hideDelay option is used. A value of 0 disables
  669. * the fade out animation.
  670. */
  671. Tooltip.prototype.hide = function (delay) {
  672. var tooltip = this;
  673. // disallow duplicate timers (#1728, #1766)
  674. U.clearTimeout(this.hideTimer);
  675. delay = pick(delay, this.options.hideDelay, 500);
  676. if (!this.isHidden) {
  677. this.hideTimer = syncTimeout(function () {
  678. // If there is a delay, do fadeOut with the default duration. If
  679. // the hideDelay is 0, we assume no animation is wanted, so we
  680. // pass 0 duration. #12994.
  681. tooltip.getLabel().fadeOut(delay ? void 0 : delay);
  682. tooltip.isHidden = true;
  683. }, delay);
  684. }
  685. };
  686. /**
  687. * @private
  688. * @function Highcharts.Tooltip#init
  689. *
  690. * @param {Highcharts.Chart} chart
  691. * The chart instance.
  692. *
  693. * @param {Highcharts.TooltipOptions} options
  694. * Tooltip options.
  695. */
  696. Tooltip.prototype.init = function (chart, options) {
  697. /**
  698. * Chart of the tooltip.
  699. *
  700. * @readonly
  701. * @name Highcharts.Tooltip#chart
  702. * @type {Highcharts.Chart}
  703. */
  704. this.chart = chart;
  705. /**
  706. * Used tooltip options.
  707. *
  708. * @readonly
  709. * @name Highcharts.Tooltip#options
  710. * @type {Highcharts.TooltipOptions}
  711. */
  712. this.options = options;
  713. /**
  714. * List of crosshairs.
  715. *
  716. * @private
  717. * @readonly
  718. * @name Highcharts.Tooltip#crosshairs
  719. * @type {Array<null>}
  720. */
  721. this.crosshairs = [];
  722. /**
  723. * Current values of x and y when animating.
  724. *
  725. * @private
  726. * @readonly
  727. * @name Highcharts.Tooltip#now
  728. * @type {Highcharts.PositionObject}
  729. */
  730. this.now = { x: 0, y: 0 };
  731. /**
  732. * Tooltips are initially hidden.
  733. *
  734. * @private
  735. * @readonly
  736. * @name Highcharts.Tooltip#isHidden
  737. * @type {boolean}
  738. */
  739. this.isHidden = true;
  740. /**
  741. * True, if the tooltip is split into one label per series, with the
  742. * header close to the axis.
  743. *
  744. * @readonly
  745. * @name Highcharts.Tooltip#split
  746. * @type {boolean|undefined}
  747. */
  748. this.split = options.split && !chart.inverted && !chart.polar;
  749. /**
  750. * When the tooltip is shared, the entire plot area will capture mouse
  751. * movement or touch events.
  752. *
  753. * @readonly
  754. * @name Highcharts.Tooltip#shared
  755. * @type {boolean|undefined}
  756. */
  757. this.shared = options.shared || this.split;
  758. /**
  759. * Whether to allow the tooltip to render outside the chart's SVG
  760. * element box. By default (false), the tooltip is rendered within the
  761. * chart's SVG element, which results in the tooltip being aligned
  762. * inside the chart area.
  763. *
  764. * @readonly
  765. * @name Highcharts.Tooltip#outside
  766. * @type {boolean}
  767. *
  768. * @todo
  769. * Split tooltip does not support outside in the first iteration. Should
  770. * not be too complicated to implement.
  771. */
  772. this.outside = pick(options.outside, Boolean(chart.scrollablePixelsX || chart.scrollablePixelsY));
  773. };
  774. /**
  775. * Returns true, if the pointer is in contact with the tooltip tracker.
  776. */
  777. Tooltip.prototype.isStickyOnContact = function () {
  778. return !!(!this.followPointer &&
  779. this.options.stickOnContact &&
  780. this.inContact);
  781. };
  782. /**
  783. * Moves the tooltip with a soft animation to a new position.
  784. *
  785. * @private
  786. * @function Highcharts.Tooltip#move
  787. *
  788. * @param {number} x
  789. *
  790. * @param {number} y
  791. *
  792. * @param {number} anchorX
  793. *
  794. * @param {number} anchorY
  795. */
  796. Tooltip.prototype.move = function (x, y, anchorX, anchorY) {
  797. var tooltip = this, now = tooltip.now, animate = tooltip.options.animation !== false &&
  798. !tooltip.isHidden &&
  799. // When we get close to the target position, abort animation and
  800. // land on the right place (#3056)
  801. (Math.abs(x - now.x) > 1 || Math.abs(y - now.y) > 1), skipAnchor = tooltip.followPointer || tooltip.len > 1;
  802. // Get intermediate values for animation
  803. extend(now, {
  804. x: animate ? (2 * now.x + x) / 3 : x,
  805. y: animate ? (now.y + y) / 2 : y,
  806. anchorX: skipAnchor ?
  807. void 0 :
  808. animate ? (2 * now.anchorX + anchorX) / 3 : anchorX,
  809. anchorY: skipAnchor ?
  810. void 0 :
  811. animate ? (now.anchorY + anchorY) / 2 : anchorY
  812. });
  813. // Move to the intermediate value
  814. tooltip.getLabel().attr(now);
  815. tooltip.drawTracker();
  816. // Run on next tick of the mouse tracker
  817. if (animate) {
  818. // Never allow two timeouts
  819. U.clearTimeout(this.tooltipTimeout);
  820. // Set the fixed interval ticking for the smooth tooltip
  821. this.tooltipTimeout = setTimeout(function () {
  822. // The interval function may still be running during destroy,
  823. // so check that the chart is really there before calling.
  824. if (tooltip) {
  825. tooltip.move(x, y, anchorX, anchorY);
  826. }
  827. }, 32);
  828. }
  829. };
  830. /**
  831. * Refresh the tooltip's text and position.
  832. *
  833. * @function Highcharts.Tooltip#refresh
  834. *
  835. * @param {Highcharts.Point|Array<Highcharts.Point>} pointOrPoints
  836. * Either a point or an array of points.
  837. *
  838. * @param {Highcharts.PointerEventObject} [mouseEvent]
  839. * Mouse event, that is responsible for the refresh and should be
  840. * used for the tooltip update.
  841. */
  842. Tooltip.prototype.refresh = function (pointOrPoints, mouseEvent) {
  843. var tooltip = this, chart = this.chart, options = tooltip.options, x, y, point = pointOrPoints, anchor, textConfig = {}, text, pointConfig = [], formatter = options.formatter || tooltip.defaultFormatter, shared = tooltip.shared, currentSeries, styledMode = chart.styledMode;
  844. if (!options.enabled) {
  845. return;
  846. }
  847. U.clearTimeout(this.hideTimer);
  848. // get the reference point coordinates (pie charts use tooltipPos)
  849. tooltip.followPointer = splat(point)[0].series.tooltipOptions
  850. .followPointer;
  851. anchor = tooltip.getAnchor(point, mouseEvent);
  852. x = anchor[0];
  853. y = anchor[1];
  854. // shared tooltip, array is sent over
  855. if (shared &&
  856. !(point.series &&
  857. point.series.noSharedTooltip)) {
  858. chart.pointer.applyInactiveState(point);
  859. // Now set hover state for the choosen ones:
  860. point.forEach(function (item) {
  861. item.setState('hover');
  862. pointConfig.push(item.getLabelConfig());
  863. });
  864. textConfig = {
  865. x: point[0].category,
  866. y: point[0].y
  867. };
  868. textConfig.points = pointConfig;
  869. point = point[0];
  870. // single point tooltip
  871. }
  872. else {
  873. textConfig = point.getLabelConfig();
  874. }
  875. this.len = pointConfig.length; // #6128
  876. text = formatter.call(textConfig, tooltip);
  877. // register the current series
  878. currentSeries = point.series;
  879. this.distance = pick(currentSeries.tooltipOptions.distance, 16);
  880. // update the inner HTML
  881. if (text === false) {
  882. this.hide();
  883. }
  884. else {
  885. // update text
  886. if (tooltip.split) {
  887. this.renderSplit(text, splat(pointOrPoints));
  888. }
  889. else {
  890. var label = tooltip.getLabel();
  891. // Prevent the tooltip from flowing over the chart box (#6659)
  892. if (!options.style.width || styledMode) {
  893. label.css({
  894. width: this.chart.spacingBox.width + 'px'
  895. });
  896. }
  897. label.attr({
  898. text: text && text.join ?
  899. text.join('') :
  900. text
  901. });
  902. // Set the stroke color of the box to reflect the point
  903. label.removeClass(/highcharts-color-[\d]+/g)
  904. .addClass('highcharts-color-' +
  905. pick(point.colorIndex, currentSeries.colorIndex));
  906. if (!styledMode) {
  907. label.attr({
  908. stroke: (options.borderColor ||
  909. point.color ||
  910. currentSeries.color ||
  911. '#666666')
  912. });
  913. }
  914. tooltip.updatePosition({
  915. plotX: x,
  916. plotY: y,
  917. negative: point.negative,
  918. ttBelow: point.ttBelow,
  919. h: anchor[2] || 0
  920. });
  921. }
  922. // show it
  923. if (tooltip.isHidden && tooltip.label) {
  924. tooltip.label.attr({
  925. opacity: 1
  926. }).show();
  927. }
  928. tooltip.isHidden = false;
  929. }
  930. fireEvent(this, 'refresh');
  931. };
  932. /**
  933. * Render the split tooltip. Loops over each point's text and adds
  934. * a label next to the point, then uses the distribute function to
  935. * find best non-overlapping positions.
  936. *
  937. * @private
  938. * @function Highcharts.Tooltip#renderSplit
  939. *
  940. * @param {string|Array<(boolean|string)>} labels
  941. *
  942. * @param {Array<Highcharts.Point>} points
  943. */
  944. Tooltip.prototype.renderSplit = function (labels, points) {
  945. var tooltip = this;
  946. var chart = tooltip.chart, _a = tooltip.chart, chartWidth = _a.chartWidth, chartHeight = _a.chartHeight, plotHeight = _a.plotHeight, plotLeft = _a.plotLeft, plotTop = _a.plotTop, pointer = _a.pointer, ren = _a.renderer, _b = _a.scrollablePixelsY, scrollablePixelsY = _b === void 0 ? 0 : _b, _c = _a.scrollingContainer, _d = _c === void 0 ? { scrollLeft: 0, scrollTop: 0 } : _c, scrollLeft = _d.scrollLeft, scrollTop = _d.scrollTop, styledMode = _a.styledMode, distance = tooltip.distance, options = tooltip.options, positioner = tooltip.options.positioner;
  947. // The area which the tooltip should be limited to. Limit to scrollable
  948. // plot area if enabled, otherwise limit to the chart container.
  949. var bounds = {
  950. left: scrollLeft,
  951. right: scrollLeft + chartWidth,
  952. top: scrollTop,
  953. bottom: scrollTop + chartHeight
  954. };
  955. var tooltipLabel = tooltip.getLabel();
  956. var headerTop = Boolean(chart.xAxis[0] && chart.xAxis[0].opposite);
  957. var distributionBoxTop = plotTop + scrollTop;
  958. var headerHeight = 0;
  959. var adjustedPlotHeight = plotHeight - scrollablePixelsY;
  960. /**
  961. * Calculates the anchor position for the partial tooltip
  962. *
  963. * @private
  964. * @param {Highcharts.Point} point The point related to the tooltip
  965. * @return {object} Returns an object with anchorX and anchorY
  966. */
  967. function getAnchor(point) {
  968. var isHeader = point.isHeader, _a = point.plotX, plotX = _a === void 0 ? 0 : _a, _b = point.plotY, plotY = _b === void 0 ? 0 : _b, series = point.series;
  969. var anchorX;
  970. var anchorY;
  971. if (isHeader) {
  972. // Set anchorX to plotX
  973. anchorX = plotLeft + plotX;
  974. // Set anchorY to center of visible plot area.
  975. anchorY = plotTop + plotHeight / 2;
  976. }
  977. else {
  978. var xAxis = series.xAxis, yAxis = series.yAxis;
  979. // Set anchorX to plotX. Limit to within xAxis.
  980. anchorX = xAxis.pos + clamp(plotX, -distance, xAxis.len + distance);
  981. // Set anchorY, limit to the scrollable plot area
  982. if (yAxis.pos + plotY >= scrollTop + plotTop &&
  983. yAxis.pos + plotY <= scrollTop + plotTop + plotHeight - scrollablePixelsY) {
  984. anchorY = yAxis.pos + plotY;
  985. }
  986. }
  987. // Limit values to plot area
  988. anchorX = clamp(anchorX, bounds.left - distance, bounds.right + distance);
  989. return { anchorX: anchorX, anchorY: anchorY };
  990. }
  991. /**
  992. * Calculates the position of the partial tooltip
  993. *
  994. * @private
  995. * @param {number} anchorX The partial tooltip anchor x position
  996. * @param {number} anchorY The partial tooltip anchor y position
  997. * @param {boolean} isHeader Whether the partial tooltip is a header
  998. * @param {number} boxWidth Width of the partial tooltip
  999. * @return {Highcharts.PositionObject} Returns the partial tooltip x and
  1000. * y position
  1001. */
  1002. function defaultPositioner(anchorX, anchorY, isHeader, boxWidth, alignedLeft) {
  1003. if (alignedLeft === void 0) { alignedLeft = true; }
  1004. var y;
  1005. var x;
  1006. if (isHeader) {
  1007. y = headerTop ? 0 : adjustedPlotHeight;
  1008. x = clamp(anchorX - (boxWidth / 2), bounds.left, bounds.right - boxWidth);
  1009. }
  1010. else {
  1011. y = anchorY - distributionBoxTop;
  1012. x = alignedLeft ?
  1013. anchorX - boxWidth - distance :
  1014. anchorX + distance;
  1015. x = clamp(x, alignedLeft ? x : bounds.left, bounds.right);
  1016. }
  1017. // NOTE: y is relative to distributionBoxTop
  1018. return { x: x, y: y };
  1019. }
  1020. /**
  1021. * Updates the attributes and styling of the partial tooltip. Creates a
  1022. * new partial tooltip if it does not exists.
  1023. *
  1024. * @private
  1025. * @param {Highcharts.SVGElement|undefined} partialTooltip
  1026. * The partial tooltip to update
  1027. * @param {Highcharts.Point} point
  1028. * The point related to the partial tooltip
  1029. * @param {boolean|string} str The text for the partial tooltip
  1030. * @return {Highcharts.SVGElement} Returns the updated partial tooltip
  1031. */
  1032. function updatePartialTooltip(partialTooltip, point, str) {
  1033. var tt = partialTooltip;
  1034. var isHeader = point.isHeader, series = point.series;
  1035. var colorClass = 'highcharts-color-' + pick(point.colorIndex, series.colorIndex, 'none');
  1036. if (!tt) {
  1037. var attribs = {
  1038. padding: options.padding,
  1039. r: options.borderRadius
  1040. };
  1041. if (!styledMode) {
  1042. attribs.fill = options.backgroundColor;
  1043. attribs['stroke-width'] = options.borderWidth;
  1044. }
  1045. tt = ren
  1046. .label('', 0, 0, (options[isHeader ? 'headerShape' : 'shape']) ||
  1047. 'callout', void 0, void 0, options.useHTML)
  1048. .addClass((isHeader ? 'highcharts-tooltip-header ' : '') +
  1049. 'highcharts-tooltip-box ' +
  1050. colorClass)
  1051. .attr(attribs)
  1052. .add(tooltipLabel);
  1053. }
  1054. tt.isActive = true;
  1055. tt.attr({
  1056. text: str
  1057. });
  1058. if (!styledMode) {
  1059. tt.css(options.style)
  1060. .shadow(options.shadow)
  1061. .attr({
  1062. stroke: (options.borderColor ||
  1063. point.color ||
  1064. series.color ||
  1065. '#333333')
  1066. });
  1067. }
  1068. return tt;
  1069. }
  1070. // Graceful degradation for legacy formatters
  1071. if (isString(labels)) {
  1072. labels = [false, labels];
  1073. }
  1074. // Create the individual labels for header and points, ignore footer
  1075. var boxes = labels.slice(0, points.length + 1).reduce(function (boxes, str, i) {
  1076. if (str !== false && str !== '') {
  1077. var point = (points[i - 1] ||
  1078. {
  1079. // Item 0 is the header. Instead of this, we could also
  1080. // use the crosshair label
  1081. isHeader: true,
  1082. plotX: points[0].plotX,
  1083. plotY: plotHeight,
  1084. series: {}
  1085. });
  1086. var isHeader = point.isHeader;
  1087. // Store the tooltip label referance on the series
  1088. var owner = isHeader ? tooltip : point.series;
  1089. var tt = owner.tt = updatePartialTooltip(owner.tt, point, str);
  1090. // Get X position now, so we can move all to the other side in
  1091. // case of overflow
  1092. var bBox = tt.getBBox();
  1093. var boxWidth = bBox.width + tt.strokeWidth();
  1094. if (isHeader) {
  1095. headerHeight = bBox.height;
  1096. adjustedPlotHeight += headerHeight;
  1097. if (headerTop) {
  1098. distributionBoxTop -= headerHeight;
  1099. }
  1100. }
  1101. var _a = getAnchor(point), anchorX = _a.anchorX, anchorY = _a.anchorY;
  1102. if (typeof anchorY === 'number') {
  1103. var size = bBox.height + 1;
  1104. var boxPosition = (positioner ?
  1105. positioner.call(tooltip, boxWidth, size, point) :
  1106. defaultPositioner(anchorX, anchorY, isHeader, boxWidth));
  1107. boxes.push({
  1108. // 0-align to the top, 1-align to the bottom
  1109. align: positioner ? 0 : void 0,
  1110. anchorX: anchorX,
  1111. anchorY: anchorY,
  1112. boxWidth: boxWidth,
  1113. point: point,
  1114. rank: pick(boxPosition.rank, isHeader ? 1 : 0),
  1115. size: size,
  1116. target: boxPosition.y,
  1117. tt: tt,
  1118. x: boxPosition.x
  1119. });
  1120. }
  1121. else {
  1122. // Hide tooltips which anchorY is outside the visible plot
  1123. // area
  1124. tt.isActive = false;
  1125. }
  1126. }
  1127. return boxes;
  1128. }, []);
  1129. // If overflow left then align all labels to the right
  1130. if (!positioner && boxes.some(function (box) { return box.x < bounds.left; })) {
  1131. boxes = boxes.map(function (box) {
  1132. var _a = defaultPositioner(box.anchorX, box.anchorY, box.point.isHeader, box.boxWidth, false), x = _a.x, y = _a.y;
  1133. return extend(box, {
  1134. target: y,
  1135. x: x
  1136. });
  1137. });
  1138. }
  1139. // Clean previous run (for missing points)
  1140. tooltip.cleanSplit();
  1141. // Distribute and put in place
  1142. H.distribute(boxes, adjustedPlotHeight);
  1143. boxes.forEach(function (box) {
  1144. var anchorX = box.anchorX, anchorY = box.anchorY, pos = box.pos, x = box.x;
  1145. // Put the label in place
  1146. box.tt.attr({
  1147. visibility: typeof pos === 'undefined' ? 'hidden' : 'inherit',
  1148. x: x,
  1149. /* NOTE: y should equal pos to be consistent with !split
  1150. * tooltip, but is currently relative to plotTop. Is left as is
  1151. * to avoid breaking change. Remove distributionBoxTop to make
  1152. * it consistent.
  1153. */
  1154. y: pos + distributionBoxTop,
  1155. anchorX: anchorX,
  1156. anchorY: anchorY
  1157. });
  1158. });
  1159. /* If we have a seperate tooltip container, then update the necessary
  1160. * container properties.
  1161. * Test that tooltip has its own container and renderer before executing
  1162. * the operation.
  1163. */
  1164. var container = tooltip.container, outside = tooltip.outside, renderer = tooltip.renderer;
  1165. if (outside && container && renderer) {
  1166. // Set container size to fit the tooltip
  1167. var _e = tooltipLabel.getBBox(), width = _e.width, height = _e.height, x = _e.x, y = _e.y;
  1168. renderer.setSize(width + x, height + y, false);
  1169. // Position the tooltip container to the chart container
  1170. var chartPosition = pointer.getChartPosition();
  1171. container.style.left = chartPosition.left + 'px';
  1172. container.style.top = chartPosition.top + 'px';
  1173. }
  1174. };
  1175. /**
  1176. * If the `stickOnContact` option is active, this will add a tracker shape.
  1177. *
  1178. * @private
  1179. * @function Highcharts.Tooltip#drawTracker
  1180. */
  1181. Tooltip.prototype.drawTracker = function () {
  1182. var tooltip = this;
  1183. if (tooltip.followPointer ||
  1184. !tooltip.options.stickOnContact) {
  1185. if (tooltip.tracker) {
  1186. tooltip.tracker.destroy();
  1187. }
  1188. return;
  1189. }
  1190. var chart = tooltip.chart;
  1191. var label = tooltip.label;
  1192. var point = chart.hoverPoint;
  1193. if (!label || !point) {
  1194. return;
  1195. }
  1196. var box = {
  1197. x: 0,
  1198. y: 0,
  1199. width: 0,
  1200. height: 0
  1201. };
  1202. // Combine anchor and tooltip
  1203. var anchorPos = this.getAnchor(point);
  1204. var labelBBox = label.getBBox();
  1205. anchorPos[0] += chart.plotLeft - label.translateX;
  1206. anchorPos[1] += chart.plotTop - label.translateY;
  1207. // When the mouse pointer is between the anchor point and the label,
  1208. // the label should stick.
  1209. box.x = Math.min(0, anchorPos[0]);
  1210. box.y = Math.min(0, anchorPos[1]);
  1211. box.width = (anchorPos[0] < 0 ?
  1212. Math.max(Math.abs(anchorPos[0]), (labelBBox.width - anchorPos[0])) :
  1213. Math.max(Math.abs(anchorPos[0]), labelBBox.width));
  1214. box.height = (anchorPos[1] < 0 ?
  1215. Math.max(Math.abs(anchorPos[1]), (labelBBox.height - Math.abs(anchorPos[1]))) :
  1216. Math.max(Math.abs(anchorPos[1]), labelBBox.height));
  1217. if (tooltip.tracker) {
  1218. tooltip.tracker.attr(box);
  1219. }
  1220. else {
  1221. tooltip.tracker = label.renderer
  1222. .rect(box)
  1223. .addClass('highcharts-tracker')
  1224. .add(label);
  1225. if (!chart.styledMode) {
  1226. tooltip.tracker.attr({
  1227. fill: 'rgba(0,0,0,0)'
  1228. });
  1229. }
  1230. }
  1231. };
  1232. /**
  1233. * @private
  1234. */
  1235. Tooltip.prototype.styledModeFormat = function (formatString) {
  1236. return formatString
  1237. .replace('style="font-size: 10px"', 'class="highcharts-header"')
  1238. .replace(/style="color:{(point|series)\.color}"/g, 'class="highcharts-color-{$1.colorIndex}"');
  1239. };
  1240. /**
  1241. * Format the footer/header of the tooltip
  1242. * #3397: abstraction to enable formatting of footer and header
  1243. *
  1244. * @private
  1245. * @function Highcharts.Tooltip#tooltipFooterHeaderFormatter
  1246. * @param {Highcharts.PointLabelObject} labelConfig
  1247. * @param {boolean} [isFooter]
  1248. * @return {string}
  1249. */
  1250. Tooltip.prototype.tooltipFooterHeaderFormatter = function (labelConfig, isFooter) {
  1251. var footOrHead = isFooter ? 'footer' : 'header', series = labelConfig.series, tooltipOptions = series.tooltipOptions, xDateFormat = tooltipOptions.xDateFormat, xAxis = series.xAxis, isDateTime = (xAxis &&
  1252. xAxis.options.type === 'datetime' &&
  1253. isNumber(labelConfig.key)), formatString = tooltipOptions[footOrHead + 'Format'], e = {
  1254. isFooter: isFooter,
  1255. labelConfig: labelConfig
  1256. };
  1257. fireEvent(this, 'headerFormatter', e, function (e) {
  1258. // Guess the best date format based on the closest point distance
  1259. // (#568, #3418)
  1260. if (isDateTime && !xDateFormat) {
  1261. xDateFormat = this.getXDateFormat(labelConfig, tooltipOptions, xAxis);
  1262. }
  1263. // Insert the footer date format if any
  1264. if (isDateTime && xDateFormat) {
  1265. ((labelConfig.point && labelConfig.point.tooltipDateKeys) ||
  1266. ['key']).forEach(function (key) {
  1267. formatString = formatString.replace('{point.' + key + '}', '{point.' + key + ':' + xDateFormat + '}');
  1268. });
  1269. }
  1270. // Replace default header style with class name
  1271. if (series.chart.styledMode) {
  1272. formatString = this.styledModeFormat(formatString);
  1273. }
  1274. e.text = format(formatString, {
  1275. point: labelConfig,
  1276. series: series
  1277. }, this.chart);
  1278. });
  1279. return e.text;
  1280. };
  1281. /**
  1282. * Updates the tooltip with the provided tooltip options.
  1283. *
  1284. * @function Highcharts.Tooltip#update
  1285. *
  1286. * @param {Highcharts.TooltipOptions} options
  1287. * The tooltip options to update.
  1288. */
  1289. Tooltip.prototype.update = function (options) {
  1290. this.destroy();
  1291. // Update user options (#6218)
  1292. merge(true, this.chart.options.tooltip.userOptions, options);
  1293. this.init(this.chart, merge(true, this.options, options));
  1294. };
  1295. /**
  1296. * Find the new position and perform the move
  1297. *
  1298. * @private
  1299. * @function Highcharts.Tooltip#updatePosition
  1300. *
  1301. * @param {Highcharts.Point} point
  1302. */
  1303. Tooltip.prototype.updatePosition = function (point) {
  1304. var chart = this.chart, pointer = chart.pointer, label = this.getLabel(), pos, anchorX = point.plotX + chart.plotLeft, anchorY = point.plotY + chart.plotTop, pad;
  1305. // Needed for outside: true (#11688)
  1306. var chartPosition = pointer.getChartPosition();
  1307. pos = (this.options.positioner || this.getPosition).call(this, label.width, label.height, point);
  1308. // Set the renderer size dynamically to prevent document size to change
  1309. if (this.outside) {
  1310. pad = (this.options.borderWidth || 0) + 2 * this.distance;
  1311. this.renderer.setSize(label.width + pad, label.height + pad, false);
  1312. // Anchor and tooltip container need scaling if chart container has
  1313. // scale transform/css zoom. #11329.
  1314. var containerScaling = chart.containerScaling;
  1315. if (containerScaling) {
  1316. css(this.container, {
  1317. transform: "scale(" + containerScaling.scaleX + ", " + containerScaling.scaleY + ")"
  1318. });
  1319. anchorX *= containerScaling.scaleX;
  1320. anchorY *= containerScaling.scaleY;
  1321. }
  1322. anchorX += chartPosition.left - pos.x;
  1323. anchorY += chartPosition.top - pos.y;
  1324. }
  1325. // do the move
  1326. this.move(Math.round(pos.x), Math.round(pos.y || 0), // can be undefined (#3977)
  1327. anchorX, anchorY);
  1328. };
  1329. return Tooltip;
  1330. }());
  1331. H.Tooltip = Tooltip;
  1332. export default H.Tooltip;