ScrollablePlotArea.js 14 KB

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