ScrollablePlotArea.js 14 KB

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