TouchPointer.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. /**
  2. * (c) 2010-2019 Torstein Honsi
  3. *
  4. * License: www.highcharts.com/license
  5. */
  6. 'use strict';
  7. import H from './Globals.js';
  8. import './Utilities.js';
  9. var charts = H.charts,
  10. extend = H.extend,
  11. noop = H.noop,
  12. pick = H.pick,
  13. Pointer = H.Pointer;
  14. // Support for touch devices
  15. extend(Pointer.prototype, /** @lends Pointer.prototype */ {
  16. /**
  17. * Run translation operations
  18. *
  19. * @private
  20. * @function Highcharts.Pointer#pinchTranslate
  21. *
  22. * @param {Array<*>} pinchDown
  23. *
  24. * @param {Array<*>} touches
  25. *
  26. * @param {*} transform
  27. *
  28. * @param {*} selectionMarker
  29. *
  30. * @param {*} clip
  31. *
  32. * @param {*} lastValidTouch
  33. */
  34. pinchTranslate: function (
  35. pinchDown,
  36. touches,
  37. transform,
  38. selectionMarker,
  39. clip,
  40. lastValidTouch
  41. ) {
  42. if (this.zoomHor) {
  43. this.pinchTranslateDirection(
  44. true,
  45. pinchDown,
  46. touches,
  47. transform,
  48. selectionMarker,
  49. clip,
  50. lastValidTouch
  51. );
  52. }
  53. if (this.zoomVert) {
  54. this.pinchTranslateDirection(
  55. false,
  56. pinchDown,
  57. touches,
  58. transform,
  59. selectionMarker,
  60. clip,
  61. lastValidTouch
  62. );
  63. }
  64. },
  65. /**
  66. * Run translation operations for each direction (horizontal and vertical)
  67. * independently.
  68. *
  69. * @private
  70. * @function Highcharts.Pointer#pinchTranslateDirection
  71. *
  72. * @param {boolean} horiz
  73. *
  74. * @param {Array<*>} pinchDown
  75. *
  76. * @param {Array<*>} touches
  77. *
  78. * @param {*} transform
  79. *
  80. * @param {*} selectionMarker
  81. *
  82. * @param {*} clip
  83. *
  84. * @param {*} lastValidTouch
  85. *
  86. * @param {number|undefined} [forcedScale=1]
  87. */
  88. pinchTranslateDirection: function (
  89. horiz,
  90. pinchDown,
  91. touches,
  92. transform,
  93. selectionMarker,
  94. clip,
  95. lastValidTouch,
  96. forcedScale
  97. ) {
  98. var chart = this.chart,
  99. xy = horiz ? 'x' : 'y',
  100. XY = horiz ? 'X' : 'Y',
  101. sChartXY = 'chart' + XY,
  102. wh = horiz ? 'width' : 'height',
  103. plotLeftTop = chart['plot' + (horiz ? 'Left' : 'Top')],
  104. selectionWH,
  105. selectionXY,
  106. clipXY,
  107. scale = forcedScale || 1,
  108. inverted = chart.inverted,
  109. bounds = chart.bounds[horiz ? 'h' : 'v'],
  110. singleTouch = pinchDown.length === 1,
  111. touch0Start = pinchDown[0][sChartXY],
  112. touch0Now = touches[0][sChartXY],
  113. touch1Start = !singleTouch && pinchDown[1][sChartXY],
  114. touch1Now = !singleTouch && touches[1][sChartXY],
  115. outOfBounds,
  116. transformScale,
  117. scaleKey,
  118. setScale = function () {
  119. // Don't zoom if fingers are too close on this axis
  120. if (!singleTouch && Math.abs(touch0Start - touch1Start) > 20) {
  121. scale = forcedScale ||
  122. Math.abs(touch0Now - touch1Now) /
  123. Math.abs(touch0Start - touch1Start);
  124. }
  125. clipXY = ((plotLeftTop - touch0Now) / scale) + touch0Start;
  126. selectionWH = chart['plot' + (horiz ? 'Width' : 'Height')] /
  127. scale;
  128. };
  129. // Set the scale, first pass
  130. setScale();
  131. // The clip position (x or y) is altered if out of bounds, the selection
  132. // position is not
  133. selectionXY = clipXY;
  134. // Out of bounds
  135. if (selectionXY < bounds.min) {
  136. selectionXY = bounds.min;
  137. outOfBounds = true;
  138. } else if (selectionXY + selectionWH > bounds.max) {
  139. selectionXY = bounds.max - selectionWH;
  140. outOfBounds = true;
  141. }
  142. // Is the chart dragged off its bounds, determined by dataMin and
  143. // dataMax?
  144. if (outOfBounds) {
  145. // Modify the touchNow position in order to create an elastic drag
  146. // movement. This indicates to the user that the chart is responsive
  147. // but can't be dragged further.
  148. touch0Now -= 0.8 * (touch0Now - lastValidTouch[xy][0]);
  149. if (!singleTouch) {
  150. touch1Now -= 0.8 * (touch1Now - lastValidTouch[xy][1]);
  151. }
  152. // Set the scale, second pass to adapt to the modified touchNow
  153. // positions
  154. setScale();
  155. } else {
  156. lastValidTouch[xy] = [touch0Now, touch1Now];
  157. }
  158. // Set geometry for clipping, selection and transformation
  159. if (!inverted) {
  160. clip[xy] = clipXY - plotLeftTop;
  161. clip[wh] = selectionWH;
  162. }
  163. scaleKey = inverted ? (horiz ? 'scaleY' : 'scaleX') : 'scale' + XY;
  164. transformScale = inverted ? 1 / scale : scale;
  165. selectionMarker[wh] = selectionWH;
  166. selectionMarker[xy] = selectionXY;
  167. transform[scaleKey] = scale;
  168. transform['translate' + XY] = (transformScale * plotLeftTop) +
  169. (touch0Now - (transformScale * touch0Start));
  170. },
  171. /**
  172. * Handle touch events with two touches
  173. *
  174. * @private
  175. * @function Highcharts.Pointer#pinch
  176. *
  177. * @param {Highcharts.PointerEvent} e
  178. */
  179. pinch: function (e) {
  180. var self = this,
  181. chart = self.chart,
  182. pinchDown = self.pinchDown,
  183. touches = e.touches,
  184. touchesLength = touches.length,
  185. lastValidTouch = self.lastValidTouch,
  186. hasZoom = self.hasZoom,
  187. selectionMarker = self.selectionMarker,
  188. transform = {},
  189. fireClickEvent = touchesLength === 1 && (
  190. (
  191. self.inClass(e.target, 'highcharts-tracker') &&
  192. chart.runTrackerClick
  193. ) ||
  194. self.runChartClick
  195. ),
  196. clip = {};
  197. // Don't initiate panning until the user has pinched. This prevents us
  198. // from blocking page scrolling as users scroll down a long page
  199. // (#4210).
  200. if (touchesLength > 1) {
  201. self.initiated = true;
  202. }
  203. // On touch devices, only proceed to trigger click if a handler is
  204. // defined
  205. if (hasZoom && self.initiated && !fireClickEvent) {
  206. e.preventDefault();
  207. }
  208. // Normalize each touch
  209. [].map.call(touches, function (e) {
  210. return self.normalize(e);
  211. });
  212. // Register the touch start position
  213. if (e.type === 'touchstart') {
  214. [].forEach.call(touches, function (e, i) {
  215. pinchDown[i] = { chartX: e.chartX, chartY: e.chartY };
  216. });
  217. lastValidTouch.x = [pinchDown[0].chartX, pinchDown[1] &&
  218. pinchDown[1].chartX];
  219. lastValidTouch.y = [pinchDown[0].chartY, pinchDown[1] &&
  220. pinchDown[1].chartY];
  221. // Identify the data bounds in pixels
  222. chart.axes.forEach(function (axis) {
  223. if (axis.zoomEnabled) {
  224. var bounds = chart.bounds[axis.horiz ? 'h' : 'v'],
  225. minPixelPadding = axis.minPixelPadding,
  226. min = axis.toPixels(
  227. pick(axis.options.min, axis.dataMin)
  228. ),
  229. max = axis.toPixels(
  230. pick(axis.options.max, axis.dataMax)
  231. ),
  232. absMin = Math.min(min, max),
  233. absMax = Math.max(min, max);
  234. // Store the bounds for use in the touchmove handler
  235. bounds.min = Math.min(axis.pos, absMin - minPixelPadding);
  236. bounds.max = Math.max(
  237. axis.pos + axis.len,
  238. absMax + minPixelPadding
  239. );
  240. }
  241. });
  242. self.res = true; // reset on next move
  243. // Optionally move the tooltip on touchmove
  244. } else if (self.followTouchMove && touchesLength === 1) {
  245. this.runPointActions(self.normalize(e));
  246. // Event type is touchmove, handle panning and pinching
  247. } else if (pinchDown.length) { // can be 0 when releasing, if touchend
  248. // fires first
  249. // Set the marker
  250. if (!selectionMarker) {
  251. self.selectionMarker = selectionMarker = extend({
  252. destroy: noop,
  253. touch: true
  254. }, chart.plotBox);
  255. }
  256. self.pinchTranslate(
  257. pinchDown,
  258. touches,
  259. transform,
  260. selectionMarker,
  261. clip,
  262. lastValidTouch
  263. );
  264. self.hasPinched = hasZoom;
  265. // Scale and translate the groups to provide visual feedback during
  266. // pinching
  267. self.scaleGroups(transform, clip);
  268. if (self.res) {
  269. self.res = false;
  270. this.reset(false, 0);
  271. }
  272. }
  273. },
  274. /**
  275. * General touch handler shared by touchstart and touchmove.
  276. *
  277. * @private
  278. * @function Highcharts.Pointer#touch
  279. *
  280. * @param {Highcharts.PointerEvent} e
  281. *
  282. * @param {boolean} start
  283. */
  284. touch: function (e, start) {
  285. var chart = this.chart,
  286. hasMoved,
  287. pinchDown,
  288. isInside;
  289. if (chart.index !== H.hoverChartIndex) {
  290. this.onContainerMouseLeave({ relatedTarget: true });
  291. }
  292. H.hoverChartIndex = chart.index;
  293. if (e.touches.length === 1) {
  294. e = this.normalize(e);
  295. isInside = chart.isInsidePlot(
  296. e.chartX - chart.plotLeft,
  297. e.chartY - chart.plotTop
  298. );
  299. if (isInside && !chart.openMenu) {
  300. // Run mouse events and display tooltip etc
  301. if (start) {
  302. this.runPointActions(e);
  303. }
  304. // Android fires touchmove events after the touchstart even if
  305. // the finger hasn't moved, or moved only a pixel or two. In iOS
  306. // however, the touchmove doesn't fire unless the finger moves
  307. // more than ~4px. So we emulate this behaviour in Android by
  308. // checking how much it moved, and cancelling on small
  309. // distances. #3450.
  310. if (e.type === 'touchmove') {
  311. pinchDown = this.pinchDown;
  312. hasMoved = pinchDown[0] ? Math.sqrt( // #5266
  313. Math.pow(pinchDown[0].chartX - e.chartX, 2) +
  314. Math.pow(pinchDown[0].chartY - e.chartY, 2)
  315. ) >= 4 : false;
  316. }
  317. if (pick(hasMoved, true)) {
  318. this.pinch(e);
  319. }
  320. } else if (start) {
  321. // Hide the tooltip on touching outside the plot area (#1203)
  322. this.reset();
  323. }
  324. } else if (e.touches.length === 2) {
  325. this.pinch(e);
  326. }
  327. },
  328. /**
  329. * @private
  330. * @function Highcharts.Pointer#onContainerTouchStart
  331. *
  332. * @param {Highcharts.PointerEvent} e
  333. */
  334. onContainerTouchStart: function (e) {
  335. this.zoomOption(e);
  336. this.touch(e, true);
  337. },
  338. /**
  339. * @private
  340. * @function Highcharts.Pointer#onContainerTouchMove
  341. *
  342. * @param {Highcharts.PointerEvent} e
  343. */
  344. onContainerTouchMove: function (e) {
  345. this.touch(e);
  346. },
  347. /**
  348. * @private
  349. * @function Highcharts.Pointer#onDocumentTouchEnd
  350. *
  351. * @param {Highcharts.PointerEvent} e
  352. */
  353. onDocumentTouchEnd: function (e) {
  354. if (charts[H.hoverChartIndex]) {
  355. charts[H.hoverChartIndex].pointer.drop(e);
  356. }
  357. }
  358. });