Tooltip.js 41 KB

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