ScrollablePlotArea.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. /* *
  2. *
  3. * (c) 2010-2019 Torstein Honsi
  4. *
  5. * License: www.highcharts.com/license
  6. *
  7. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  8. *
  9. * Highcharts feature to make the Y axis stay fixed when scrolling the chart
  10. * horizontally on mobile devices. Supports left and right side axes.
  11. */
  12. /*
  13. WIP on vertical scrollable plot area (#9378). To do:
  14. - Bottom axis positioning
  15. - Test with Gantt
  16. - Look for size optimizing the code
  17. - API and demos
  18. */
  19. 'use strict';
  20. import H from './Globals.js';
  21. import U from './Utilities.js';
  22. var pick = U.pick;
  23. var addEvent = H.addEvent, Chart = H.Chart;
  24. /**
  25. * Options for a scrollable plot area. This feature provides a minimum size for
  26. * the plot area of the chart. If the size gets smaller than this, typically
  27. * on mobile devices, a native browser scrollbar is presented. This scrollbar
  28. * provides smooth scrolling for the contents of the plot area, whereas the
  29. * title, legend and unaffected axes are fixed.
  30. *
  31. * Since v7.1.2, a scrollable plot area can be defined for either horizontal or
  32. * vertical scrolling, depending on whether the `minWidth` or `minHeight`
  33. * option is set.
  34. *
  35. * @sample highcharts/chart/scrollable-plotarea
  36. * Scrollable plot area
  37. * @sample highcharts/chart/scrollable-plotarea-vertical
  38. * Vertically scrollable plot area
  39. * @sample {gantt} highcharts/chart/scrollable-plotarea-vertical
  40. * Gantt chart with vertically scrollable plot area
  41. *
  42. * @since 6.1.0
  43. * @product highcharts gantt
  44. * @apioption chart.scrollablePlotArea
  45. */
  46. /**
  47. * The minimum height for the plot area. If it gets smaller than this, the plot
  48. * area will become scrollable.
  49. *
  50. * @type {number}
  51. * @apioption chart.scrollablePlotArea.minHeight
  52. */
  53. /**
  54. * The minimum width for the plot area. If it gets smaller than this, the plot
  55. * area will become scrollable.
  56. *
  57. * @type {number}
  58. * @apioption chart.scrollablePlotArea.minWidth
  59. */
  60. /**
  61. * The initial scrolling position of the scrollable plot area. Ranges from 0 to
  62. * 1, where 0 aligns the plot area to the left and 1 aligns it to the right.
  63. * Typically we would use 1 if the chart has right aligned Y axes.
  64. *
  65. * @type {number}
  66. * @apioption chart.scrollablePlotArea.scrollPositionX
  67. */
  68. /**
  69. * The initial scrolling position of the scrollable plot area. Ranges from 0 to
  70. * 1, where 0 aligns the plot area to the top and 1 aligns it to the bottom.
  71. *
  72. * @type {number}
  73. * @apioption chart.scrollablePlotArea.scrollPositionY
  74. */
  75. /**
  76. * The opacity of mask applied on one of the sides of the plot
  77. * area.
  78. *
  79. * @sample {highcharts} highcharts/chart/scrollable-plotarea-opacity
  80. * Disabled opacity for the mask
  81. *
  82. * @type {number}
  83. * @default 0.85
  84. * @since 7.1.1
  85. * @apioption chart.scrollablePlotArea.opacity
  86. */
  87. ''; // detach API doclets
  88. /* eslint-disable no-invalid-this, valid-jsdoc */
  89. addEvent(Chart, 'afterSetChartSize', function (e) {
  90. var scrollablePlotArea = this.options.chart.scrollablePlotArea, scrollableMinWidth = scrollablePlotArea && scrollablePlotArea.minWidth, scrollableMinHeight = scrollablePlotArea && scrollablePlotArea.minHeight, scrollablePixelsX, scrollablePixelsY, corrections;
  91. if (!this.renderer.forExport) {
  92. // The amount of pixels to scroll, the difference between chart
  93. // width and scrollable width
  94. if (scrollableMinWidth) {
  95. this.scrollablePixelsX = scrollablePixelsX = Math.max(0, scrollableMinWidth - this.chartWidth);
  96. if (scrollablePixelsX) {
  97. this.plotWidth += scrollablePixelsX;
  98. if (this.inverted) {
  99. this.clipBox.height += scrollablePixelsX;
  100. this.plotBox.height += scrollablePixelsX;
  101. }
  102. else {
  103. this.clipBox.width += scrollablePixelsX;
  104. this.plotBox.width += scrollablePixelsX;
  105. }
  106. corrections = {
  107. // Corrections for right side
  108. 1: { name: 'right', value: scrollablePixelsX }
  109. };
  110. }
  111. // Currently we can only do either X or Y
  112. }
  113. else if (scrollableMinHeight) {
  114. this.scrollablePixelsY = scrollablePixelsY = Math.max(0, scrollableMinHeight - this.chartHeight);
  115. if (scrollablePixelsY) {
  116. this.plotHeight += scrollablePixelsY;
  117. if (this.inverted) {
  118. this.clipBox.width += scrollablePixelsY;
  119. this.plotBox.width += scrollablePixelsY;
  120. }
  121. else {
  122. this.clipBox.height += scrollablePixelsY;
  123. this.plotBox.height += scrollablePixelsY;
  124. }
  125. corrections = {
  126. 2: { name: 'bottom', value: scrollablePixelsY }
  127. };
  128. }
  129. }
  130. if (corrections && !e.skipAxes) {
  131. this.axes.forEach(function (axis) {
  132. // For right and bottom axes, only fix the plot line length
  133. if (corrections[axis.side]) {
  134. // Get the plot lines right in getPlotLinePath,
  135. // temporarily set it to the adjusted plot width.
  136. axis.getPlotLinePath = function () {
  137. var marginName = corrections[axis.side].name, correctionValue = corrections[axis.side].value,
  138. // axis.right or axis.bottom
  139. margin = this[marginName], path;
  140. // Temporarily adjust
  141. this[marginName] = margin - correctionValue;
  142. path = H.Axis.prototype.getPlotLinePath.apply(this, arguments);
  143. // Reset
  144. this[marginName] = margin;
  145. return path;
  146. };
  147. }
  148. else {
  149. // Apply the corrected plotWidth
  150. axis.setAxisSize();
  151. axis.setAxisTranslation();
  152. }
  153. });
  154. }
  155. }
  156. });
  157. addEvent(Chart, 'render', function () {
  158. if (this.scrollablePixelsX || this.scrollablePixelsY) {
  159. if (this.setUpScrolling) {
  160. this.setUpScrolling();
  161. }
  162. this.applyFixed();
  163. }
  164. else if (this.fixedDiv) { // Has been in scrollable mode
  165. this.applyFixed();
  166. }
  167. });
  168. /**
  169. * @private
  170. * @function Highcharts.Chart#setUpScrolling
  171. * @return {void}
  172. */
  173. Chart.prototype.setUpScrolling = function () {
  174. var attribs = {
  175. WebkitOverflowScrolling: 'touch',
  176. overflowX: 'hidden',
  177. overflowY: 'hidden'
  178. };
  179. if (this.scrollablePixelsX) {
  180. attribs.overflowX = 'auto';
  181. }
  182. if (this.scrollablePixelsY) {
  183. attribs.overflowY = 'auto';
  184. }
  185. // Add the necessary divs to provide scrolling
  186. this.scrollingContainer = H.createElement('div', {
  187. 'className': 'highcharts-scrolling'
  188. }, attribs, this.renderTo);
  189. this.innerContainer = H.createElement('div', {
  190. 'className': 'highcharts-inner-container'
  191. }, null, this.scrollingContainer);
  192. // Now move the container inside
  193. this.innerContainer.appendChild(this.container);
  194. // Don't run again
  195. this.setUpScrolling = null;
  196. };
  197. /**
  198. * These elements are moved over to the fixed renderer and stay fixed when the
  199. * user scrolls the chart
  200. * @private
  201. */
  202. Chart.prototype.moveFixedElements = function () {
  203. var container = this.container, fixedRenderer = this.fixedRenderer, fixedSelectors = [
  204. '.highcharts-contextbutton',
  205. '.highcharts-credits',
  206. '.highcharts-legend',
  207. '.highcharts-legend-checkbox',
  208. '.highcharts-navigator-series',
  209. '.highcharts-navigator-xaxis',
  210. '.highcharts-navigator-yaxis',
  211. '.highcharts-navigator',
  212. '.highcharts-reset-zoom',
  213. '.highcharts-scrollbar',
  214. '.highcharts-subtitle',
  215. '.highcharts-title'
  216. ], axisClass;
  217. if (this.scrollablePixelsX && !this.inverted) {
  218. axisClass = '.highcharts-yaxis';
  219. }
  220. else if (this.scrollablePixelsX && this.inverted) {
  221. axisClass = '.highcharts-xaxis';
  222. }
  223. else if (this.scrollablePixelsY && !this.inverted) {
  224. axisClass = '.highcharts-xaxis';
  225. }
  226. else if (this.scrollablePixelsY && this.inverted) {
  227. axisClass = '.highcharts-yaxis';
  228. }
  229. fixedSelectors.push(axisClass, axisClass + '-labels');
  230. fixedSelectors.forEach(function (className) {
  231. [].forEach.call(container.querySelectorAll(className), function (elem) {
  232. (elem.namespaceURI === fixedRenderer.SVG_NS ?
  233. fixedRenderer.box :
  234. fixedRenderer.box.parentNode).appendChild(elem);
  235. elem.style.pointerEvents = 'auto';
  236. });
  237. });
  238. };
  239. /**
  240. * @private
  241. * @function Highcharts.Chart#applyFixed
  242. * @return {void}
  243. */
  244. Chart.prototype.applyFixed = function () {
  245. var fixedRenderer, scrollableWidth, scrollableHeight, firstTime = !this.fixedDiv, scrollableOptions = this.options.chart.scrollablePlotArea;
  246. // First render
  247. if (firstTime) {
  248. this.fixedDiv = H.createElement('div', {
  249. className: 'highcharts-fixed'
  250. }, {
  251. position: 'absolute',
  252. overflow: 'hidden',
  253. pointerEvents: 'none',
  254. zIndex: 2
  255. }, null, true);
  256. this.renderTo.insertBefore(this.fixedDiv, this.renderTo.firstChild);
  257. this.renderTo.style.overflow = 'visible';
  258. this.fixedRenderer = fixedRenderer = new H.Renderer(this.fixedDiv, this.chartWidth, this.chartHeight);
  259. // Mask
  260. this.scrollableMask = fixedRenderer
  261. .path()
  262. .attr({
  263. fill: this.options.chart.backgroundColor || '#fff',
  264. 'fill-opacity': pick(scrollableOptions.opacity, 0.85),
  265. zIndex: -1
  266. })
  267. .addClass('highcharts-scrollable-mask')
  268. .add();
  269. this.moveFixedElements();
  270. addEvent(this, 'afterShowResetZoom', this.moveFixedElements);
  271. addEvent(this, 'afterLayOutTitles', this.moveFixedElements);
  272. }
  273. else {
  274. // Set the size of the fixed renderer to the visible width
  275. this.fixedRenderer.setSize(this.chartWidth, this.chartHeight);
  276. }
  277. // Increase the size of the scrollable renderer and background
  278. scrollableWidth = this.chartWidth + (this.scrollablePixelsX || 0);
  279. scrollableHeight = this.chartHeight + (this.scrollablePixelsY || 0);
  280. H.stop(this.container);
  281. this.container.style.width = scrollableWidth + 'px';
  282. this.container.style.height = scrollableHeight + 'px';
  283. this.renderer.boxWrapper.attr({
  284. width: scrollableWidth,
  285. height: scrollableHeight,
  286. viewBox: [0, 0, scrollableWidth, scrollableHeight].join(' ')
  287. });
  288. this.chartBackground.attr({
  289. width: scrollableWidth,
  290. height: scrollableHeight
  291. });
  292. if (this.scrollablePixelsY) {
  293. this.scrollingContainer.style.height = this.chartHeight + 'px';
  294. }
  295. // Set scroll position
  296. if (firstTime) {
  297. if (scrollableOptions.scrollPositionX) {
  298. this.scrollingContainer.scrollLeft =
  299. this.scrollablePixelsX *
  300. scrollableOptions.scrollPositionX;
  301. }
  302. if (scrollableOptions.scrollPositionY) {
  303. this.scrollingContainer.scrollTop =
  304. this.scrollablePixelsY *
  305. scrollableOptions.scrollPositionY;
  306. }
  307. }
  308. // Mask behind the left and right side
  309. var axisOffset = this.axisOffset, maskTop = this.plotTop - axisOffset[0] - 1, maskLeft = this.plotLeft - axisOffset[3] - 1, maskBottom = this.plotTop + this.plotHeight + axisOffset[2] + 1, maskRight = this.plotLeft + this.plotWidth + axisOffset[1] + 1, maskPlotRight = this.plotLeft + this.plotWidth -
  310. (this.scrollablePixelsX || 0), maskPlotBottom = this.plotTop + this.plotHeight -
  311. (this.scrollablePixelsY || 0), d;
  312. if (this.scrollablePixelsX) {
  313. d = [
  314. // Left side
  315. 'M', 0, maskTop,
  316. 'L', this.plotLeft - 1, maskTop,
  317. 'L', this.plotLeft - 1, maskBottom,
  318. 'L', 0, maskBottom,
  319. 'Z',
  320. // Right side
  321. 'M', maskPlotRight, maskTop,
  322. 'L', this.chartWidth, maskTop,
  323. 'L', this.chartWidth, maskBottom,
  324. 'L', maskPlotRight, maskBottom,
  325. 'Z'
  326. ];
  327. }
  328. else if (this.scrollablePixelsY) {
  329. d = [
  330. // Top side
  331. 'M', maskLeft, 0,
  332. 'L', maskLeft, this.plotTop - 1,
  333. 'L', maskRight, this.plotTop - 1,
  334. 'L', maskRight, 0,
  335. 'Z',
  336. // Bottom side
  337. 'M', maskLeft, maskPlotBottom,
  338. 'L', maskLeft, this.chartHeight,
  339. 'L', maskRight, this.chartHeight,
  340. 'L', maskRight, maskPlotBottom,
  341. 'Z'
  342. ];
  343. }
  344. else {
  345. d = ['M', 0, 0];
  346. }
  347. if (this.redrawTrigger !== 'adjustHeight') {
  348. this.scrollableMask.attr({
  349. d: d
  350. });
  351. }
  352. };