Pointer.js 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324
  1. /**
  2. * (c) 2010-2019 Torstein Honsi
  3. *
  4. * License: www.highcharts.com/license
  5. */
  6. /**
  7. * One position in relation to an axis.
  8. *
  9. * @interface Highcharts.PointerAxisCoordinateObject
  10. *//**
  11. * Related axis.
  12. *
  13. * @name Highcharts.PointerAxisCoordinateObject#axis
  14. * @type {Highcharts.Axis}
  15. *//**
  16. * Axis value.
  17. *
  18. * @name Highcharts.PointerAxisCoordinateObject#value
  19. * @type {number}
  20. */
  21. /**
  22. * Positions in terms of axis values.
  23. *
  24. * @interface Highcharts.PointerAxisCoordinatesObject
  25. *//**
  26. * Positions on the x-axis.
  27. * @name Highcharts.PointerAxisCoordinatesObject#xAxis
  28. * @type {Array<Highcharts.PointerAxisCoordinateObject>}
  29. *//**
  30. * Positions on the y-axis.
  31. * @name Highcharts.PointerAxisCoordinatesObject#yAxis
  32. * @type {Array<Highcharts.PointerAxisCoordinateObject>}
  33. */
  34. /**
  35. * Pointer coordinates.
  36. *
  37. * @interface Highcharts.PointerCoordinatesObject
  38. *//**
  39. * @name Highcharts.PointerCoordinatesObject#chartX
  40. * @type {number}
  41. *//**
  42. * @name Highcharts.PointerCoordinatesObject#chartY
  43. * @type {number}
  44. */
  45. /**
  46. * A native browser mouse or touch event, extended with position information
  47. * relative to the {@link Chart.container}.
  48. *
  49. * @interface Highcharts.PointerEventObject
  50. * @extends global.PointerEvent
  51. *//**
  52. * The X coordinate of the pointer interaction relative to the chart.
  53. *
  54. * @name Highcharts.PointerEventObject#chartX
  55. * @type {number}
  56. *//**
  57. * The Y coordinate of the pointer interaction relative to the chart.
  58. *
  59. * @name Highcharts.PointerEventObject#chartY
  60. * @type {number}
  61. */
  62. /**
  63. * Axis-specific data of a selection.
  64. *
  65. * @interface Highcharts.SelectDataObject
  66. *//**
  67. * @name Highcharts.SelectDataObject#axis
  68. * @type {Highcharts.Axis}
  69. *//**
  70. * @name Highcharts.SelectDataObject#min
  71. * @type {number}
  72. *//**
  73. * @name Highcharts.SelectDataObject#max
  74. * @type {number}
  75. */
  76. /**
  77. * Object for select events.
  78. *
  79. * @interface Highcharts.SelectEventObject
  80. *//**
  81. * @name Highcharts.SelectEventObject#originalEvent
  82. * @type {global.Event}
  83. *//**
  84. * @name Highcharts.SelectEventObject#xAxis
  85. * @type {Array<Highcharts.SelectDataObject>}
  86. *//**
  87. * @name Highcharts.SelectEventObject#yAxis
  88. * @type {Array<Highcharts.SelectDataObject>}
  89. */
  90. 'use strict';
  91. import Highcharts from './Globals.js';
  92. import './Utilities.js';
  93. import './Tooltip.js';
  94. import './Color.js';
  95. var H = Highcharts,
  96. addEvent = H.addEvent,
  97. attr = H.attr,
  98. charts = H.charts,
  99. color = H.color,
  100. css = H.css,
  101. defined = H.defined,
  102. extend = H.extend,
  103. find = H.find,
  104. fireEvent = H.fireEvent,
  105. isNumber = H.isNumber,
  106. isObject = H.isObject,
  107. offset = H.offset,
  108. pick = H.pick,
  109. splat = H.splat,
  110. Tooltip = H.Tooltip;
  111. /**
  112. * The mouse and touch tracker object. Each {@link Chart} item has one
  113. * assosiated Pointer item that can be accessed from the {@link Chart.pointer}
  114. * property.
  115. *
  116. * @class
  117. * @name Highcharts.Pointer
  118. *
  119. * @param {Highcharts.Chart} chart
  120. * The Chart instance.
  121. *
  122. * @param {Highcharts.Options} options
  123. * The root options object. The pointer uses options from the chart and
  124. * tooltip structures.
  125. */
  126. Highcharts.Pointer = function (chart, options) {
  127. this.init(chart, options);
  128. };
  129. Highcharts.Pointer.prototype = {
  130. /**
  131. * Initialize the Pointer.
  132. *
  133. * @private
  134. * @function Highcharts.Pointer#init
  135. *
  136. * @param {Highcharts.Chart} chart
  137. * The Chart instance.
  138. *
  139. * @param {Highcharts.Options} options
  140. * The root options object. The pointer uses options from the chart
  141. * and tooltip structures.
  142. */
  143. init: function (chart, options) {
  144. // Store references
  145. this.options = options;
  146. this.chart = chart;
  147. // Do we need to handle click on a touch device?
  148. this.runChartClick =
  149. options.chart.events && !!options.chart.events.click;
  150. this.pinchDown = [];
  151. this.lastValidTouch = {};
  152. if (Tooltip) {
  153. /**
  154. * Tooltip object for points of series.
  155. *
  156. * @name Highcharts.Chart#tooltip
  157. * @type {Highcharts.Tooltip}
  158. */
  159. chart.tooltip = new Tooltip(chart, options.tooltip);
  160. this.followTouchMove = pick(options.tooltip.followTouchMove, true);
  161. }
  162. this.setDOMEvents();
  163. },
  164. /**
  165. * Resolve the zoomType option, this is reset on all touch start and mouse
  166. * down events.
  167. *
  168. * @private
  169. * @function Highcharts.Pointer#zoomOption
  170. *
  171. * @param {global.Event} e
  172. * Event object.
  173. */
  174. zoomOption: function (e) {
  175. var chart = this.chart,
  176. options = chart.options.chart,
  177. zoomType = options.zoomType || '',
  178. inverted = chart.inverted,
  179. zoomX,
  180. zoomY;
  181. // Look for the pinchType option
  182. if (/touch/.test(e.type)) {
  183. zoomType = pick(options.pinchType, zoomType);
  184. }
  185. this.zoomX = zoomX = /x/.test(zoomType);
  186. this.zoomY = zoomY = /y/.test(zoomType);
  187. this.zoomHor = (zoomX && !inverted) || (zoomY && inverted);
  188. this.zoomVert = (zoomY && !inverted) || (zoomX && inverted);
  189. this.hasZoom = zoomX || zoomY;
  190. },
  191. /**
  192. * Takes a browser event object and extends it with custom Highcharts
  193. * properties `chartX` and `chartY` in order to work on the internal
  194. * coordinate system.
  195. *
  196. * @function Highcharts.Pointer#normalize
  197. *
  198. * @param {global.Event} e
  199. * Event object in standard browsers.
  200. *
  201. * @return {Highcharts.PointerEventObject}
  202. * A browser event with extended properties `chartX` and `chartY`.
  203. */
  204. normalize: function (e, chartPosition) {
  205. var ePos;
  206. // iOS (#2757)
  207. ePos = e.touches ?
  208. (e.touches.length ? e.touches.item(0) : e.changedTouches[0]) :
  209. e;
  210. // Get mouse position
  211. if (!chartPosition) {
  212. this.chartPosition = chartPosition = offset(this.chart.container);
  213. }
  214. return extend(e, {
  215. chartX: Math.round(ePos.pageX - chartPosition.left),
  216. chartY: Math.round(ePos.pageY - chartPosition.top)
  217. });
  218. },
  219. /**
  220. * Get the click position in terms of axis values.
  221. *
  222. * @function Highcharts.Pointer#getCoordinates
  223. *
  224. * @param {Highcharts.PointerEventObject} e
  225. * Pointer event, extended with `chartX` and `chartY` properties.
  226. *
  227. * @return {Highcharts.PointerAxisCoordinatesObject}
  228. */
  229. getCoordinates: function (e) {
  230. var coordinates = {
  231. xAxis: [],
  232. yAxis: []
  233. };
  234. this.chart.axes.forEach(function (axis) {
  235. coordinates[axis.isXAxis ? 'xAxis' : 'yAxis'].push({
  236. axis: axis,
  237. value: axis.toValue(e[axis.horiz ? 'chartX' : 'chartY'])
  238. });
  239. });
  240. return coordinates;
  241. },
  242. /**
  243. * Finds the closest point to a set of coordinates, using the k-d-tree
  244. * algorithm.
  245. *
  246. * @function Highcharts.Pointer#findNearestKDPoints
  247. *
  248. * @param {Array<Highcharts.Series>} series
  249. * All the series to search in.
  250. *
  251. * @param {boolean} shared
  252. * Whether it is a shared tooltip or not.
  253. *
  254. * @param {Highcharts.PointerEventObject} e
  255. * The pointer event object, containing chart coordinates of the
  256. * pointer.
  257. *
  258. * @return {Point|undefined}
  259. * The point closest to given coordinates.
  260. */
  261. findNearestKDPoint: function (series, shared, e) {
  262. var closest,
  263. sort = function (p1, p2) {
  264. var isCloserX = p1.distX - p2.distX,
  265. isCloser = p1.dist - p2.dist,
  266. isAbove =
  267. (p2.series.group && p2.series.group.zIndex) -
  268. (p1.series.group && p1.series.group.zIndex),
  269. result;
  270. // We have two points which are not in the same place on xAxis
  271. // and shared tooltip:
  272. if (isCloserX !== 0 && shared) { // #5721
  273. result = isCloserX;
  274. // Points are not exactly in the same place on x/yAxis:
  275. } else if (isCloser !== 0) {
  276. result = isCloser;
  277. // The same xAxis and yAxis position, sort by z-index:
  278. } else if (isAbove !== 0) {
  279. result = isAbove;
  280. // The same zIndex, sort by array index:
  281. } else {
  282. result = p1.series.index > p2.series.index ? -1 : 1;
  283. }
  284. return result;
  285. };
  286. series.forEach(function (s) {
  287. var noSharedTooltip = s.noSharedTooltip && shared,
  288. compareX = (
  289. !noSharedTooltip &&
  290. s.options.findNearestPointBy.indexOf('y') < 0
  291. ),
  292. point = s.searchPoint(
  293. e,
  294. compareX
  295. );
  296. if (
  297. // Check that we actually found a point on the series.
  298. isObject(point, true) &&
  299. // Use the new point if it is closer.
  300. (!isObject(closest, true) || (sort(closest, point) > 0))
  301. ) {
  302. closest = point;
  303. }
  304. });
  305. return closest;
  306. },
  307. /**
  308. * @private
  309. * @function Highcharts.Pointer#getPointFromEvent
  310. *
  311. * @param {global.Event} e
  312. *
  313. * @return {Highcharts.Point|undefined}
  314. */
  315. getPointFromEvent: function (e) {
  316. var target = e.target,
  317. point;
  318. while (target && !point) {
  319. point = target.point;
  320. target = target.parentNode;
  321. }
  322. return point;
  323. },
  324. /**
  325. * @private
  326. * @function Highcharts.Pointer#getChartCoordinatesFromPoint
  327. *
  328. * @param {Highcharts.Point} point
  329. *
  330. * @param {boolean} inverted
  331. *
  332. * @return {Highcharts.PointerCoordinatesObject}
  333. */
  334. getChartCoordinatesFromPoint: function (point, inverted) {
  335. var series = point.series,
  336. xAxis = series.xAxis,
  337. yAxis = series.yAxis,
  338. plotX = pick(point.clientX, point.plotX),
  339. shapeArgs = point.shapeArgs;
  340. if (xAxis && yAxis) {
  341. return inverted ? {
  342. chartX: xAxis.len + xAxis.pos - plotX,
  343. chartY: yAxis.len + yAxis.pos - point.plotY
  344. } : {
  345. chartX: plotX + xAxis.pos,
  346. chartY: point.plotY + yAxis.pos
  347. };
  348. }
  349. if (shapeArgs && shapeArgs.x && shapeArgs.y) {
  350. // E.g. pies do not have axes
  351. return {
  352. chartX: shapeArgs.x,
  353. chartY: shapeArgs.y
  354. };
  355. }
  356. },
  357. /**
  358. * Calculates what is the current hovered point/points and series.
  359. *
  360. * @private
  361. * @function Highcharts.Pointer#getHoverData
  362. *
  363. * @param {Highcharts.Point|undefined} existingHoverPoint
  364. * The point currrently beeing hovered.
  365. *
  366. * @param {Highcharts.Series|undefined} existingHoverSeries
  367. * The series currently beeing hovered.
  368. *
  369. * @param {Array<Highcharts.Series>} series
  370. * All the series in the chart.
  371. *
  372. * @param {boolean} isDirectTouch
  373. * Is the pointer directly hovering the point.
  374. *
  375. * @param {boolean} shared
  376. * Whether it is a shared tooltip or not.
  377. *
  378. * @param {Highcharts.PointerEventObject} e
  379. * The triggering event, containing chart coordinates of the pointer.
  380. *
  381. * @return {*}
  382. * Object containing resulting hover data: hoverPoint, hoverSeries,
  383. * and hoverPoints.
  384. */
  385. getHoverData: function (
  386. existingHoverPoint,
  387. existingHoverSeries,
  388. series,
  389. isDirectTouch,
  390. shared,
  391. e
  392. ) {
  393. var hoverPoint,
  394. hoverPoints = [],
  395. hoverSeries = existingHoverSeries,
  396. useExisting = !!(isDirectTouch && existingHoverPoint),
  397. notSticky = hoverSeries && !hoverSeries.stickyTracking,
  398. filter = function (s) {
  399. return (
  400. s.visible &&
  401. !(!shared && s.directTouch) && // #3821
  402. pick(s.options.enableMouseTracking, true)
  403. );
  404. },
  405. // Which series to look in for the hover point
  406. searchSeries = notSticky ?
  407. // Only search on hovered series if it has stickyTracking false
  408. [hoverSeries] :
  409. // Filter what series to look in.
  410. series.filter(function (s) {
  411. return filter(s) && s.stickyTracking;
  412. });
  413. // Use existing hovered point or find the one closest to coordinates.
  414. hoverPoint = useExisting ?
  415. existingHoverPoint :
  416. this.findNearestKDPoint(searchSeries, shared, e);
  417. // Assign hover series
  418. hoverSeries = hoverPoint && hoverPoint.series;
  419. // If we have a hoverPoint, assign hoverPoints.
  420. if (hoverPoint) {
  421. // When tooltip is shared, it displays more than one point
  422. if (shared && !hoverSeries.noSharedTooltip) {
  423. searchSeries = series.filter(function (s) {
  424. return filter(s) && !s.noSharedTooltip;
  425. });
  426. // Get all points with the same x value as the hoverPoint
  427. searchSeries.forEach(function (s) {
  428. var point = find(s.points, function (p) {
  429. return p.x === hoverPoint.x && !p.isNull;
  430. });
  431. if (isObject(point)) {
  432. /*
  433. * Boost returns a minimal point. Convert it to a usable
  434. * point for tooltip and states.
  435. */
  436. if (s.chart.isBoosting) {
  437. point = s.getPoint(point);
  438. }
  439. hoverPoints.push(point);
  440. }
  441. });
  442. } else {
  443. hoverPoints.push(hoverPoint);
  444. }
  445. }
  446. return {
  447. hoverPoint: hoverPoint,
  448. hoverSeries: hoverSeries,
  449. hoverPoints: hoverPoints
  450. };
  451. },
  452. /**
  453. * With line type charts with a single tracker, get the point closest to the
  454. * mouse. Run Point.onMouseOver and display tooltip for the point or points.
  455. *
  456. * @private
  457. * @function Highcharts.Pointer#runPointActions
  458. *
  459. * @param {global.Event} e
  460. *
  461. * @param {Highcharts.Point} p
  462. *
  463. * @fires Highcharts.Point#event:mouseOut
  464. * @fires Highcharts.Point#event:mouseOver
  465. */
  466. runPointActions: function (e, p) {
  467. var pointer = this,
  468. chart = pointer.chart,
  469. series = chart.series,
  470. tooltip = chart.tooltip && chart.tooltip.options.enabled ?
  471. chart.tooltip :
  472. undefined,
  473. shared = tooltip ? tooltip.shared : false,
  474. hoverPoint = p || chart.hoverPoint,
  475. hoverSeries = hoverPoint && hoverPoint.series || chart.hoverSeries,
  476. // onMouseOver or already hovering a series with directTouch
  477. isDirectTouch = e.type !== 'touchmove' && (
  478. !!p || (
  479. (hoverSeries && hoverSeries.directTouch) &&
  480. pointer.isDirectTouch
  481. )
  482. ),
  483. hoverData = this.getHoverData(
  484. hoverPoint,
  485. hoverSeries,
  486. series,
  487. isDirectTouch,
  488. shared,
  489. e
  490. ),
  491. useSharedTooltip,
  492. followPointer,
  493. anchor,
  494. points;
  495. // Update variables from hoverData.
  496. hoverPoint = hoverData.hoverPoint;
  497. points = hoverData.hoverPoints;
  498. hoverSeries = hoverData.hoverSeries;
  499. followPointer = hoverSeries && hoverSeries.tooltipOptions.followPointer;
  500. useSharedTooltip = (
  501. shared &&
  502. hoverSeries &&
  503. !hoverSeries.noSharedTooltip
  504. );
  505. // Refresh tooltip for kdpoint if new hover point or tooltip was hidden
  506. // #3926, #4200
  507. if (
  508. hoverPoint &&
  509. // !(hoverSeries && hoverSeries.directTouch) &&
  510. (hoverPoint !== chart.hoverPoint || (tooltip && tooltip.isHidden))
  511. ) {
  512. (chart.hoverPoints || []).forEach(function (p) {
  513. if (points.indexOf(p) === -1) {
  514. p.setState();
  515. }
  516. });
  517. // Do mouseover on all points (#3919, #3985, #4410, #5622)
  518. (points || []).forEach(function (p) {
  519. p.setState('hover');
  520. });
  521. // set normal state to previous series
  522. if (chart.hoverSeries !== hoverSeries) {
  523. hoverSeries.onMouseOver();
  524. }
  525. // If tracking is on series in stead of on each point,
  526. // fire mouseOver on hover point. // #4448
  527. if (chart.hoverPoint) {
  528. chart.hoverPoint.firePointEvent('mouseOut');
  529. }
  530. // Hover point may have been destroyed in the event handlers (#7127)
  531. if (!hoverPoint.series) {
  532. return;
  533. }
  534. hoverPoint.firePointEvent('mouseOver');
  535. chart.hoverPoints = points;
  536. chart.hoverPoint = hoverPoint;
  537. // Draw tooltip if necessary
  538. if (tooltip) {
  539. tooltip.refresh(useSharedTooltip ? points : hoverPoint, e);
  540. }
  541. // Update positions (regardless of kdpoint or hoverPoint)
  542. } else if (followPointer && tooltip && !tooltip.isHidden) {
  543. anchor = tooltip.getAnchor([{}], e);
  544. tooltip.updatePosition({ plotX: anchor[0], plotY: anchor[1] });
  545. }
  546. // Start the event listener to pick up the tooltip and crosshairs
  547. if (!pointer.unDocMouseMove) {
  548. pointer.unDocMouseMove = addEvent(
  549. chart.container.ownerDocument,
  550. 'mousemove',
  551. function (e) {
  552. var chart = charts[H.hoverChartIndex];
  553. if (chart) {
  554. chart.pointer.onDocumentMouseMove(e);
  555. }
  556. }
  557. );
  558. }
  559. // Issues related to crosshair #4927, #5269 #5066, #5658
  560. chart.axes.forEach(function drawAxisCrosshair(axis) {
  561. var snap = pick(axis.crosshair.snap, true),
  562. point = !snap ?
  563. undefined :
  564. H.find(points, function (p) {
  565. return p.series[axis.coll] === axis;
  566. });
  567. // Axis has snapping crosshairs, and one of the hover points belongs
  568. // to axis. Always call drawCrosshair when it is not snap.
  569. if (point || !snap) {
  570. axis.drawCrosshair(e, point);
  571. // Axis has snapping crosshairs, but no hover point belongs to axis
  572. } else {
  573. axis.hideCrosshair();
  574. }
  575. });
  576. },
  577. /**
  578. * Reset the tracking by hiding the tooltip, the hover series state and the
  579. * hover point
  580. *
  581. * @function Highcharts.Pointer#reset
  582. *
  583. * @param {boolean} allowMove
  584. * Instead of destroying the tooltip altogether, allow moving it if
  585. * possible.
  586. *
  587. * @param {number} delay
  588. */
  589. reset: function (allowMove, delay) {
  590. var pointer = this,
  591. chart = pointer.chart,
  592. hoverSeries = chart.hoverSeries,
  593. hoverPoint = chart.hoverPoint,
  594. hoverPoints = chart.hoverPoints,
  595. tooltip = chart.tooltip,
  596. tooltipPoints = tooltip && tooltip.shared ?
  597. hoverPoints :
  598. hoverPoint;
  599. // Check if the points have moved outside the plot area (#1003, #4736,
  600. // #5101)
  601. if (allowMove && tooltipPoints) {
  602. splat(tooltipPoints).forEach(function (point) {
  603. if (point.series.isCartesian && point.plotX === undefined) {
  604. allowMove = false;
  605. }
  606. });
  607. }
  608. // Just move the tooltip, #349
  609. if (allowMove) {
  610. if (tooltip && tooltipPoints && tooltipPoints.length) {
  611. tooltip.refresh(tooltipPoints);
  612. if (tooltip.shared && hoverPoints) { // #8284
  613. hoverPoints.forEach(function (point) {
  614. point.setState(point.state, true);
  615. if (point.series.isCartesian) {
  616. if (point.series.xAxis.crosshair) {
  617. point.series.xAxis.drawCrosshair(null, point);
  618. }
  619. if (point.series.yAxis.crosshair) {
  620. point.series.yAxis.drawCrosshair(null, point);
  621. }
  622. }
  623. });
  624. } else if (hoverPoint) { // #2500
  625. hoverPoint.setState(hoverPoint.state, true);
  626. chart.axes.forEach(function (axis) {
  627. if (axis.crosshair) {
  628. axis.drawCrosshair(null, hoverPoint);
  629. }
  630. });
  631. }
  632. }
  633. // Full reset
  634. } else {
  635. if (hoverPoint) {
  636. hoverPoint.onMouseOut();
  637. }
  638. if (hoverPoints) {
  639. hoverPoints.forEach(function (point) {
  640. point.setState();
  641. });
  642. }
  643. if (hoverSeries) {
  644. hoverSeries.onMouseOut();
  645. }
  646. if (tooltip) {
  647. tooltip.hide(delay);
  648. }
  649. if (pointer.unDocMouseMove) {
  650. pointer.unDocMouseMove = pointer.unDocMouseMove();
  651. }
  652. // Remove crosshairs
  653. chart.axes.forEach(function (axis) {
  654. axis.hideCrosshair();
  655. });
  656. pointer.hoverX = chart.hoverPoints = chart.hoverPoint = null;
  657. }
  658. },
  659. /**
  660. * Scale series groups to a certain scale and translation.
  661. *
  662. * @private
  663. * @function Highcharts.Pointer#scaleGroups
  664. *
  665. * @param {Highcharts.SeriesPlotBoxObject} attribs
  666. *
  667. * @param {boolean} clip
  668. */
  669. scaleGroups: function (attribs, clip) {
  670. var chart = this.chart,
  671. seriesAttribs;
  672. // Scale each series
  673. chart.series.forEach(function (series) {
  674. seriesAttribs = attribs || series.getPlotBox(); // #1701
  675. if (series.xAxis && series.xAxis.zoomEnabled && series.group) {
  676. series.group.attr(seriesAttribs);
  677. if (series.markerGroup) {
  678. series.markerGroup.attr(seriesAttribs);
  679. series.markerGroup.clip(clip ? chart.clipRect : null);
  680. }
  681. if (series.dataLabelsGroup) {
  682. series.dataLabelsGroup.attr(seriesAttribs);
  683. }
  684. }
  685. });
  686. // Clip
  687. chart.clipRect.attr(clip || chart.clipBox);
  688. },
  689. /**
  690. * Start a drag operation.
  691. *
  692. * @private
  693. * @function Highcharts.Pointer#dragStart
  694. *
  695. * @param {Highcharts.PointerEventObject} e
  696. */
  697. dragStart: function (e) {
  698. var chart = this.chart;
  699. // Record the start position
  700. chart.mouseIsDown = e.type;
  701. chart.cancelClick = false;
  702. chart.mouseDownX = this.mouseDownX = e.chartX;
  703. chart.mouseDownY = this.mouseDownY = e.chartY;
  704. },
  705. /**
  706. * Perform a drag operation in response to a mousemove event while the mouse
  707. * is down.
  708. *
  709. * @private
  710. * @function Highcharts.Pointer#drag
  711. *
  712. * @param {Highcharts.PointerEventObject} e
  713. */
  714. drag: function (e) {
  715. var chart = this.chart,
  716. chartOptions = chart.options.chart,
  717. chartX = e.chartX,
  718. chartY = e.chartY,
  719. zoomHor = this.zoomHor,
  720. zoomVert = this.zoomVert,
  721. plotLeft = chart.plotLeft,
  722. plotTop = chart.plotTop,
  723. plotWidth = chart.plotWidth,
  724. plotHeight = chart.plotHeight,
  725. clickedInside,
  726. size,
  727. selectionMarker = this.selectionMarker,
  728. mouseDownX = this.mouseDownX,
  729. mouseDownY = this.mouseDownY,
  730. panKey = chartOptions.panKey && e[chartOptions.panKey + 'Key'];
  731. // If the device supports both touch and mouse (like IE11), and we are
  732. // touch-dragging inside the plot area, don't handle the mouse event.
  733. // #4339.
  734. if (selectionMarker && selectionMarker.touch) {
  735. return;
  736. }
  737. // If the mouse is outside the plot area, adjust to cooordinates
  738. // inside to prevent the selection marker from going outside
  739. if (chartX < plotLeft) {
  740. chartX = plotLeft;
  741. } else if (chartX > plotLeft + plotWidth) {
  742. chartX = plotLeft + plotWidth;
  743. }
  744. if (chartY < plotTop) {
  745. chartY = plotTop;
  746. } else if (chartY > plotTop + plotHeight) {
  747. chartY = plotTop + plotHeight;
  748. }
  749. // determine if the mouse has moved more than 10px
  750. this.hasDragged = Math.sqrt(
  751. Math.pow(mouseDownX - chartX, 2) +
  752. Math.pow(mouseDownY - chartY, 2)
  753. );
  754. if (this.hasDragged > 10) {
  755. clickedInside = chart.isInsidePlot(
  756. mouseDownX - plotLeft,
  757. mouseDownY - plotTop
  758. );
  759. // make a selection
  760. if (
  761. chart.hasCartesianSeries &&
  762. (this.zoomX || this.zoomY) &&
  763. clickedInside &&
  764. !panKey
  765. ) {
  766. if (!selectionMarker) {
  767. this.selectionMarker = selectionMarker =
  768. chart.renderer.rect(
  769. plotLeft,
  770. plotTop,
  771. zoomHor ? 1 : plotWidth,
  772. zoomVert ? 1 : plotHeight,
  773. 0
  774. )
  775. .attr({
  776. 'class': 'highcharts-selection-marker',
  777. 'zIndex': 7
  778. })
  779. .add();
  780. if (!chart.styledMode) {
  781. selectionMarker.attr({
  782. fill: (
  783. chartOptions.selectionMarkerFill ||
  784. color('#335cad')
  785. .setOpacity(0.25).get()
  786. )
  787. });
  788. }
  789. }
  790. }
  791. // adjust the width of the selection marker
  792. if (selectionMarker && zoomHor) {
  793. size = chartX - mouseDownX;
  794. selectionMarker.attr({
  795. width: Math.abs(size),
  796. x: (size > 0 ? 0 : size) + mouseDownX
  797. });
  798. }
  799. // adjust the height of the selection marker
  800. if (selectionMarker && zoomVert) {
  801. size = chartY - mouseDownY;
  802. selectionMarker.attr({
  803. height: Math.abs(size),
  804. y: (size > 0 ? 0 : size) + mouseDownY
  805. });
  806. }
  807. // panning
  808. if (clickedInside && !selectionMarker && chartOptions.panning) {
  809. chart.pan(e, chartOptions.panning);
  810. }
  811. }
  812. },
  813. /**
  814. * On mouse up or touch end across the entire document, drop the selection.
  815. *
  816. * @private
  817. * @function Highcharts.Pointer#drop
  818. *
  819. * @param {global.Event} e
  820. */
  821. drop: function (e) {
  822. var pointer = this,
  823. chart = this.chart,
  824. hasPinched = this.hasPinched;
  825. if (this.selectionMarker) {
  826. var selectionData = {
  827. originalEvent: e, // #4890
  828. xAxis: [],
  829. yAxis: []
  830. },
  831. selectionBox = this.selectionMarker,
  832. selectionLeft = selectionBox.attr ?
  833. selectionBox.attr('x') :
  834. selectionBox.x,
  835. selectionTop = selectionBox.attr ?
  836. selectionBox.attr('y') :
  837. selectionBox.y,
  838. selectionWidth = selectionBox.attr ?
  839. selectionBox.attr('width') :
  840. selectionBox.width,
  841. selectionHeight = selectionBox.attr ?
  842. selectionBox.attr('height') :
  843. selectionBox.height,
  844. runZoom;
  845. // a selection has been made
  846. if (this.hasDragged || hasPinched) {
  847. // record each axis' min and max
  848. chart.axes.forEach(function (axis) {
  849. if (
  850. axis.zoomEnabled &&
  851. defined(axis.min) &&
  852. (
  853. hasPinched ||
  854. pointer[{
  855. xAxis: 'zoomX',
  856. yAxis: 'zoomY'
  857. }[axis.coll]]
  858. )
  859. ) { // #859, #3569
  860. var horiz = axis.horiz,
  861. minPixelPadding = e.type === 'touchend' ?
  862. axis.minPixelPadding :
  863. 0, // #1207, #3075
  864. selectionMin = axis.toValue(
  865. (horiz ? selectionLeft : selectionTop) +
  866. minPixelPadding
  867. ),
  868. selectionMax = axis.toValue(
  869. (
  870. horiz ?
  871. selectionLeft + selectionWidth :
  872. selectionTop + selectionHeight
  873. ) - minPixelPadding
  874. );
  875. selectionData[axis.coll].push({
  876. axis: axis,
  877. // Min/max for reversed axes
  878. min: Math.min(selectionMin, selectionMax),
  879. max: Math.max(selectionMin, selectionMax)
  880. });
  881. runZoom = true;
  882. }
  883. });
  884. if (runZoom) {
  885. fireEvent(
  886. chart,
  887. 'selection',
  888. selectionData,
  889. function (args) {
  890. chart.zoom(
  891. extend(
  892. args,
  893. hasPinched ? { animation: false } : null
  894. )
  895. );
  896. }
  897. );
  898. }
  899. }
  900. if (isNumber(chart.index)) {
  901. this.selectionMarker = this.selectionMarker.destroy();
  902. }
  903. // Reset scaling preview
  904. if (hasPinched) {
  905. this.scaleGroups();
  906. }
  907. }
  908. // Reset all. Check isNumber because it may be destroyed on mouse up
  909. // (#877)
  910. if (chart && isNumber(chart.index)) {
  911. css(chart.container, { cursor: chart._cursor });
  912. chart.cancelClick = this.hasDragged > 10; // #370
  913. chart.mouseIsDown = this.hasDragged = this.hasPinched = false;
  914. this.pinchDown = [];
  915. }
  916. },
  917. /**
  918. * @private
  919. * @function Highcharts.Pointer#onContainerMouseDown
  920. *
  921. * @param {global.Event} e
  922. */
  923. onContainerMouseDown: function (e) {
  924. // Normalize before the 'if' for the legacy IE (#7850)
  925. e = this.normalize(e);
  926. if (e.button !== 2) {
  927. this.zoomOption(e);
  928. // issue #295, dragging not always working in Firefox
  929. if (e.preventDefault) {
  930. e.preventDefault();
  931. }
  932. this.dragStart(e);
  933. }
  934. },
  935. /**
  936. * @private
  937. * @function Highcharts.Pointer#onDocumentMouseUp
  938. *
  939. * @param {global.Event} e
  940. */
  941. onDocumentMouseUp: function (e) {
  942. if (charts[H.hoverChartIndex]) {
  943. charts[H.hoverChartIndex].pointer.drop(e);
  944. }
  945. },
  946. /**
  947. * Special handler for mouse move that will hide the tooltip when the mouse
  948. * leaves the plotarea. Issue #149 workaround. The mouseleave event does not
  949. * always fire.
  950. *
  951. * @private
  952. * @function Highcharts.Pointer#onDocumentMouseMove
  953. *
  954. * @param {Highcharts.PointerEventObject} e
  955. */
  956. onDocumentMouseMove: function (e) {
  957. var chart = this.chart,
  958. chartPosition = this.chartPosition;
  959. e = this.normalize(e, chartPosition);
  960. // If we're outside, hide the tooltip
  961. if (
  962. chartPosition &&
  963. !this.inClass(e.target, 'highcharts-tracker') &&
  964. !chart.isInsidePlot(
  965. e.chartX - chart.plotLeft,
  966. e.chartY - chart.plotTop
  967. )
  968. ) {
  969. this.reset();
  970. }
  971. },
  972. /**
  973. * When mouse leaves the container, hide the tooltip.
  974. *
  975. * @private
  976. * @function Highcharts.Pointer#onContainerMouseLeave
  977. *
  978. * @param {global.Event} e
  979. */
  980. onContainerMouseLeave: function (e) {
  981. var chart = charts[H.hoverChartIndex];
  982. // #4886, MS Touch end fires mouseleave but with no related target
  983. if (chart && (e.relatedTarget || e.toElement)) {
  984. chart.pointer.reset();
  985. // Also reset the chart position, used in #149 fix
  986. chart.pointer.chartPosition = null;
  987. }
  988. },
  989. /**
  990. * The mousemove, touchmove and touchstart event handler
  991. *
  992. * @private
  993. * @function Highcharts.Pointer#onContainerMouseMove
  994. *
  995. * @param {Highcharts.PointerEventObject} e
  996. */
  997. onContainerMouseMove: function (e) {
  998. var chart = this.chart;
  999. if (
  1000. !defined(H.hoverChartIndex) ||
  1001. !charts[H.hoverChartIndex] ||
  1002. !charts[H.hoverChartIndex].mouseIsDown
  1003. ) {
  1004. H.hoverChartIndex = chart.index;
  1005. }
  1006. e = this.normalize(e);
  1007. e.returnValue = false; // #2251, #3224
  1008. if (chart.mouseIsDown === 'mousedown') {
  1009. this.drag(e);
  1010. }
  1011. // Show the tooltip and run mouse over events (#977)
  1012. if (
  1013. (
  1014. this.inClass(e.target, 'highcharts-tracker') ||
  1015. chart.isInsidePlot(
  1016. e.chartX - chart.plotLeft,
  1017. e.chartY - chart.plotTop
  1018. )
  1019. ) &&
  1020. !chart.openMenu
  1021. ) {
  1022. this.runPointActions(e);
  1023. }
  1024. },
  1025. /**
  1026. * Utility to detect whether an element has, or has a parent with, a
  1027. * specificclass name. Used on detection of tracker objects and on deciding
  1028. * whether hovering the tooltip should cause the active series to mouse out.
  1029. *
  1030. * @function Highcharts.Pointer#inClass
  1031. *
  1032. * @param {Highcharts.SVGDOMElement|Highcharts.HTMLDOMElement} element
  1033. * The element to investigate.
  1034. *
  1035. * @param {string} className
  1036. * The class name to look for.
  1037. *
  1038. * @return {boolean}
  1039. * True if either the element or one of its parents has the given
  1040. * class name.
  1041. */
  1042. inClass: function (element, className) {
  1043. var elemClassName;
  1044. while (element) {
  1045. elemClassName = attr(element, 'class');
  1046. if (elemClassName) {
  1047. if (elemClassName.indexOf(className) !== -1) {
  1048. return true;
  1049. }
  1050. if (elemClassName.indexOf('highcharts-container') !== -1) {
  1051. return false;
  1052. }
  1053. }
  1054. element = element.parentNode;
  1055. }
  1056. },
  1057. /**
  1058. * @private
  1059. * @function Highcharts.Pointer#onTrackerMouseOut
  1060. *
  1061. * @param {global.Event} e
  1062. */
  1063. onTrackerMouseOut: function (e) {
  1064. var series = this.chart.hoverSeries,
  1065. relatedTarget = e.relatedTarget || e.toElement;
  1066. this.isDirectTouch = false;
  1067. if (
  1068. series &&
  1069. relatedTarget &&
  1070. !series.stickyTracking &&
  1071. !this.inClass(relatedTarget, 'highcharts-tooltip') &&
  1072. (
  1073. !this.inClass(
  1074. relatedTarget,
  1075. 'highcharts-series-' + series.index
  1076. ) || // #2499, #4465
  1077. !this.inClass(relatedTarget, 'highcharts-tracker') // #5553
  1078. )
  1079. ) {
  1080. series.onMouseOut();
  1081. }
  1082. },
  1083. /**
  1084. * @private
  1085. * @function Highcharts.Pointer#onContainerClick
  1086. *
  1087. * @param {Highcharts.PointerEventObject} e
  1088. */
  1089. onContainerClick: function (e) {
  1090. var chart = this.chart,
  1091. hoverPoint = chart.hoverPoint,
  1092. plotLeft = chart.plotLeft,
  1093. plotTop = chart.plotTop;
  1094. e = this.normalize(e);
  1095. if (!chart.cancelClick) {
  1096. // On tracker click, fire the series and point events. #783, #1583
  1097. if (hoverPoint && this.inClass(e.target, 'highcharts-tracker')) {
  1098. // the series click event
  1099. fireEvent(hoverPoint.series, 'click', extend(e, {
  1100. point: hoverPoint
  1101. }));
  1102. // the point click event
  1103. if (chart.hoverPoint) { // it may be destroyed (#1844)
  1104. hoverPoint.firePointEvent('click', e);
  1105. }
  1106. // When clicking outside a tracker, fire a chart event
  1107. } else {
  1108. extend(e, this.getCoordinates(e));
  1109. // fire a click event in the chart
  1110. if (
  1111. chart.isInsidePlot(e.chartX - plotLeft, e.chartY - plotTop)
  1112. ) {
  1113. fireEvent(chart, 'click', e);
  1114. }
  1115. }
  1116. }
  1117. },
  1118. /**
  1119. * Set the JS DOM events on the container and document. This method should
  1120. * contain a one-to-one assignment between methods and their handlers. Any
  1121. * advanced logic should be moved to the handler reflecting the event's
  1122. * name.
  1123. *
  1124. * @private
  1125. * @function Highcharts.Pointer#setDOMEvents
  1126. */
  1127. setDOMEvents: function () {
  1128. var pointer = this,
  1129. container = pointer.chart.container,
  1130. ownerDoc = container.ownerDocument;
  1131. container.onmousedown = function (e) {
  1132. pointer.onContainerMouseDown(e);
  1133. };
  1134. container.onmousemove = function (e) {
  1135. pointer.onContainerMouseMove(e);
  1136. };
  1137. container.onclick = function (e) {
  1138. pointer.onContainerClick(e);
  1139. };
  1140. this.unbindContainerMouseLeave = addEvent(
  1141. container,
  1142. 'mouseleave',
  1143. pointer.onContainerMouseLeave
  1144. );
  1145. if (!H.unbindDocumentMouseUp) {
  1146. H.unbindDocumentMouseUp = addEvent(
  1147. ownerDoc,
  1148. 'mouseup',
  1149. pointer.onDocumentMouseUp
  1150. );
  1151. }
  1152. if (H.hasTouch) {
  1153. container.ontouchstart = function (e) {
  1154. pointer.onContainerTouchStart(e);
  1155. };
  1156. container.ontouchmove = function (e) {
  1157. pointer.onContainerTouchMove(e);
  1158. };
  1159. if (!H.unbindDocumentTouchEnd) {
  1160. H.unbindDocumentTouchEnd = addEvent(
  1161. ownerDoc,
  1162. 'touchend',
  1163. pointer.onDocumentTouchEnd
  1164. );
  1165. }
  1166. }
  1167. },
  1168. /**
  1169. * Destroys the Pointer object and disconnects DOM events.
  1170. *
  1171. * @function Highcharts.Pointer#destroy
  1172. */
  1173. destroy: function () {
  1174. var pointer = this;
  1175. if (pointer.unDocMouseMove) {
  1176. pointer.unDocMouseMove();
  1177. }
  1178. this.unbindContainerMouseLeave();
  1179. if (!H.chartCount) {
  1180. if (H.unbindDocumentMouseUp) {
  1181. H.unbindDocumentMouseUp = H.unbindDocumentMouseUp();
  1182. }
  1183. if (H.unbindDocumentTouchEnd) {
  1184. H.unbindDocumentTouchEnd = H.unbindDocumentTouchEnd();
  1185. }
  1186. }
  1187. // memory and CPU leak
  1188. clearInterval(pointer.tooltipTimeout);
  1189. H.objectEach(pointer, function (val, prop) {
  1190. pointer[prop] = null;
  1191. });
  1192. }
  1193. };