DragPanes.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. /* *
  2. *
  3. * Plugin for resizing axes / panes in a chart.
  4. *
  5. * (c) 2010-2021 Highsoft AS
  6. *
  7. * Author: Kacper Madej
  8. *
  9. * License: www.highcharts.com/license
  10. *
  11. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  12. *
  13. * */
  14. 'use strict';
  15. import H from '../Core/Globals.js';
  16. var hasTouch = H.hasTouch;
  17. import Axis from '../Core/Axis/Axis.js';
  18. import palette from '../Core/Color/Palette.js';
  19. import Pointer from '../Core/Pointer.js';
  20. import U from '../Core/Utilities.js';
  21. var addEvent = U.addEvent, clamp = U.clamp, isNumber = U.isNumber, merge = U.merge, objectEach = U.objectEach, relativeLength = U.relativeLength, wrap = U.wrap;
  22. /* eslint-disable no-invalid-this, valid-jsdoc */
  23. /**
  24. * The AxisResizer class.
  25. *
  26. * @private
  27. * @class
  28. * @name Highcharts.AxisResizer
  29. *
  30. * @param {Highcharts.Axis} axis
  31. * Main axis for the AxisResizer.
  32. */
  33. var AxisResizer = /** @class */ (function () {
  34. function AxisResizer(axis) {
  35. /* eslint-enable no-invalid-this */
  36. this.axis = void 0;
  37. this.controlLine = void 0;
  38. this.lastPos = void 0;
  39. this.options = void 0;
  40. this.init(axis);
  41. }
  42. /**
  43. * Initialize the AxisResizer object.
  44. *
  45. * @function Highcharts.AxisResizer#init
  46. *
  47. * @param {Highcharts.Axis} axis
  48. * Main axis for the AxisResizer.
  49. */
  50. AxisResizer.prototype.init = function (axis, update) {
  51. this.axis = axis;
  52. this.options = axis.options.resize;
  53. this.render();
  54. if (!update) {
  55. // Add mouse events.
  56. this.addMouseEvents();
  57. }
  58. };
  59. /**
  60. * Render the AxisResizer
  61. *
  62. * @function Highcharts.AxisResizer#render
  63. */
  64. AxisResizer.prototype.render = function () {
  65. var resizer = this, axis = resizer.axis, chart = axis.chart, options = resizer.options, x = options.x || 0, y = options.y,
  66. // Normalize control line position according to the plot area
  67. pos = clamp(axis.top + axis.height + y, chart.plotTop, chart.plotTop + chart.plotHeight), attr = {}, lineWidth;
  68. if (!chart.styledMode) {
  69. attr = {
  70. cursor: options.cursor,
  71. stroke: options.lineColor,
  72. 'stroke-width': options.lineWidth,
  73. dashstyle: options.lineDashStyle
  74. };
  75. }
  76. // Register current position for future reference.
  77. resizer.lastPos = pos - y;
  78. if (!resizer.controlLine) {
  79. resizer.controlLine = chart.renderer.path()
  80. .addClass('highcharts-axis-resizer');
  81. }
  82. // Add to axisGroup after axis update, because the group is recreated
  83. // Do .add() before path is calculated because strokeWidth() needs it.
  84. resizer.controlLine.add(axis.axisGroup);
  85. lineWidth = chart.styledMode ?
  86. resizer.controlLine.strokeWidth() :
  87. options.lineWidth;
  88. attr.d = chart.renderer.crispLine([
  89. ['M', axis.left + x, pos],
  90. ['L', axis.left + axis.width + x, pos]
  91. ], lineWidth);
  92. resizer.controlLine.attr(attr);
  93. };
  94. /**
  95. * Set up the mouse and touch events for the control line.
  96. *
  97. * @function Highcharts.AxisResizer#addMouseEvents
  98. */
  99. AxisResizer.prototype.addMouseEvents = function () {
  100. var resizer = this, ctrlLineElem = resizer.controlLine.element, container = resizer.axis.chart.container, eventsToUnbind = [], mouseMoveHandler, mouseUpHandler, mouseDownHandler;
  101. // Create mouse events' handlers.
  102. // Make them as separate functions to enable wrapping them:
  103. resizer.mouseMoveHandler = mouseMoveHandler = function (e) {
  104. resizer.onMouseMove(e);
  105. };
  106. resizer.mouseUpHandler = mouseUpHandler = function (e) {
  107. resizer.onMouseUp(e);
  108. };
  109. resizer.mouseDownHandler = mouseDownHandler = function (e) {
  110. resizer.onMouseDown(e);
  111. };
  112. // Add mouse move and mouseup events. These are bind to doc/container,
  113. // because resizer.grabbed flag is stored in mousedown events.
  114. eventsToUnbind.push(addEvent(container, 'mousemove', mouseMoveHandler), addEvent(container.ownerDocument, 'mouseup', mouseUpHandler), addEvent(ctrlLineElem, 'mousedown', mouseDownHandler));
  115. // Touch events.
  116. if (hasTouch) {
  117. eventsToUnbind.push(addEvent(container, 'touchmove', mouseMoveHandler), addEvent(container.ownerDocument, 'touchend', mouseUpHandler), addEvent(ctrlLineElem, 'touchstart', mouseDownHandler));
  118. }
  119. resizer.eventsToUnbind = eventsToUnbind;
  120. };
  121. /**
  122. * Mouse move event based on x/y mouse position.
  123. *
  124. * @function Highcharts.AxisResizer#onMouseMove
  125. *
  126. * @param {Highcharts.PointerEventObject} e
  127. * Mouse event.
  128. */
  129. AxisResizer.prototype.onMouseMove = function (e) {
  130. /*
  131. * In iOS, a mousemove event with e.pageX === 0 is fired when holding
  132. * the finger down in the center of the scrollbar. This should
  133. * be ignored. Borrowed from Navigator.
  134. */
  135. if (!e.touches || e.touches[0].pageX !== 0) {
  136. // Drag the control line
  137. if (this.grabbed) {
  138. this.hasDragged = true;
  139. this.updateAxes(this.axis.chart.pointer.normalize(e).chartY -
  140. this.options.y);
  141. }
  142. }
  143. };
  144. /**
  145. * Mouse up event based on x/y mouse position.
  146. *
  147. * @function Highcharts.AxisResizer#onMouseUp
  148. *
  149. * @param {Highcharts.PointerEventObject} e
  150. * Mouse event.
  151. */
  152. AxisResizer.prototype.onMouseUp = function (e) {
  153. if (this.hasDragged) {
  154. this.updateAxes(this.axis.chart.pointer.normalize(e).chartY -
  155. this.options.y);
  156. }
  157. // Restore runPointActions.
  158. this.grabbed = this.hasDragged = this.axis.chart.activeResizer =
  159. null;
  160. };
  161. /**
  162. * Mousedown on a control line.
  163. * Will store necessary information for drag&drop.
  164. *
  165. * @function Highcharts.AxisResizer#onMouseDown
  166. */
  167. AxisResizer.prototype.onMouseDown = function (e) {
  168. // Clear all hover effects.
  169. this.axis.chart.pointer.reset(false, 0);
  170. // Disable runPointActions.
  171. this.grabbed = this.axis.chart.activeResizer = true;
  172. };
  173. /**
  174. * Update all connected axes after a change of control line position
  175. *
  176. * @function Highcharts.AxisResizer#updateAxes
  177. *
  178. * @param {number} chartY
  179. */
  180. AxisResizer.prototype.updateAxes = function (chartY) {
  181. var resizer = this, chart = resizer.axis.chart, axes = resizer.options.controlledAxis, nextAxes = axes.next.length === 0 ?
  182. [chart.yAxis.indexOf(resizer.axis) + 1] : axes.next,
  183. // Main axis is included in the prev array by default
  184. prevAxes = [resizer.axis].concat(axes.prev),
  185. // prev and next configs
  186. axesConfigs = [], stopDrag = false, plotTop = chart.plotTop, plotHeight = chart.plotHeight, plotBottom = plotTop + plotHeight, yDelta, calculatePercent = function (value) {
  187. return value * 100 / plotHeight + '%';
  188. }, normalize = function (val, min, max) {
  189. return Math.round(clamp(val, min, max));
  190. };
  191. // Normalize chartY to plot area limits
  192. chartY = clamp(chartY, plotTop, plotBottom);
  193. yDelta = chartY - resizer.lastPos;
  194. // Update on changes of at least 1 pixel in the desired direction
  195. if (yDelta * yDelta < 1) {
  196. return;
  197. }
  198. // First gather info how axes should behave
  199. [prevAxes, nextAxes].forEach(function (axesGroup, isNext) {
  200. axesGroup.forEach(function (axisInfo, i) {
  201. // Axes given as array index, axis object or axis id
  202. var axis = isNumber(axisInfo) ?
  203. // If it's a number - it's an index
  204. chart.yAxis[axisInfo] :
  205. (
  206. // If it's first elem. in first group
  207. (!isNext && !i) ?
  208. // then it's an Axis object
  209. axisInfo :
  210. // else it should be an id
  211. chart.get(axisInfo)), axisOptions = axis && axis.options, optionsToUpdate = {}, hDelta = 0, height, top, minLength, maxLength;
  212. // Skip if axis is not found
  213. // or it is navigator's yAxis (#7732)
  214. if (!axisOptions ||
  215. axisOptions.id === 'navigator-y-axis') {
  216. return;
  217. }
  218. top = axis.top;
  219. minLength = Math.round(relativeLength(axisOptions.minLength, plotHeight));
  220. maxLength = Math.round(relativeLength(axisOptions.maxLength, plotHeight));
  221. if (isNext) {
  222. // Try to change height first. yDelta could had changed
  223. yDelta = chartY - resizer.lastPos;
  224. // Normalize height to option limits
  225. height = normalize(axis.len - yDelta, minLength, maxLength);
  226. // Adjust top, so the axis looks like shrinked from top
  227. top = axis.top + yDelta;
  228. // Check for plot area limits
  229. if (top + height > plotBottom) {
  230. hDelta = plotBottom - height - top;
  231. chartY += hDelta;
  232. top += hDelta;
  233. }
  234. // Fit to plot - when overflowing on top
  235. if (top < plotTop) {
  236. top = plotTop;
  237. if (top + height > plotBottom) {
  238. height = plotHeight;
  239. }
  240. }
  241. // If next axis meets min length, stop dragging:
  242. if (height === minLength) {
  243. stopDrag = true;
  244. }
  245. axesConfigs.push({
  246. axis: axis,
  247. options: {
  248. top: calculatePercent(top - plotTop),
  249. height: calculatePercent(height)
  250. }
  251. });
  252. }
  253. else {
  254. // Normalize height to option limits
  255. height = normalize(chartY - top, minLength, maxLength);
  256. // If prev axis meets max length, stop dragging:
  257. if (height === maxLength) {
  258. stopDrag = true;
  259. }
  260. // Check axis size limits
  261. chartY = top + height;
  262. axesConfigs.push({
  263. axis: axis,
  264. options: {
  265. height: calculatePercent(height)
  266. }
  267. });
  268. }
  269. optionsToUpdate.height = height;
  270. });
  271. });
  272. // If we hit the min/maxLength with dragging, don't do anything:
  273. if (!stopDrag) {
  274. // Now update axes:
  275. axesConfigs.forEach(function (config) {
  276. config.axis.update(config.options, false);
  277. });
  278. chart.redraw(false);
  279. }
  280. };
  281. /**
  282. * Destroy AxisResizer. Clear outside references, clear events,
  283. * destroy elements, nullify properties.
  284. *
  285. * @function Highcharts.AxisResizer#destroy
  286. */
  287. AxisResizer.prototype.destroy = function () {
  288. var resizer = this, axis = resizer.axis;
  289. // Clear resizer in axis
  290. delete axis.resizer;
  291. // Clear control line events
  292. if (this.eventsToUnbind) {
  293. this.eventsToUnbind.forEach(function (unbind) {
  294. unbind();
  295. });
  296. }
  297. // Destroy AxisResizer elements
  298. resizer.controlLine.destroy();
  299. // Nullify properties
  300. objectEach(resizer, function (val, key) {
  301. resizer[key] = null;
  302. });
  303. };
  304. // Default options for AxisResizer.
  305. AxisResizer.resizerOptions = {
  306. /**
  307. * Minimal size of a resizable axis. Could be set as a percent
  308. * of plot area or pixel size.
  309. *
  310. * @sample {highstock} stock/yaxis/resize-min-max-length
  311. * minLength and maxLength
  312. *
  313. * @type {number|string}
  314. * @product highstock
  315. * @requires modules/drag-panes
  316. * @apioption yAxis.minLength
  317. */
  318. minLength: '10%',
  319. /**
  320. * Maximal size of a resizable axis. Could be set as a percent
  321. * of plot area or pixel size.
  322. *
  323. * @sample {highstock} stock/yaxis/resize-min-max-length
  324. * minLength and maxLength
  325. *
  326. * @type {number|string}
  327. * @product highstock
  328. * @requires modules/drag-panes
  329. * @apioption yAxis.maxLength
  330. */
  331. maxLength: '100%',
  332. /**
  333. * Options for axis resizing. It adds a thick line between panes which
  334. * the user can drag in order to resize the panes.
  335. *
  336. * @sample {highstock} stock/demo/candlestick-and-volume
  337. * Axis resizing enabled
  338. *
  339. * @product highstock
  340. * @requires modules/drag-panes
  341. * @optionparent yAxis.resize
  342. */
  343. resize: {
  344. /**
  345. * Contains two arrays of axes that are controlled by control line
  346. * of the axis.
  347. *
  348. * @requires modules/drag-panes
  349. */
  350. controlledAxis: {
  351. /**
  352. * Array of axes that should move out of the way of resizing
  353. * being done for the current axis. If not set, the next axis
  354. * will be used.
  355. *
  356. * @sample {highstock} stock/yaxis/multiple-resizers
  357. * Three panes with resizers
  358. * @sample {highstock} stock/yaxis/resize-multiple-axes
  359. * One resizer controlling multiple axes
  360. *
  361. * @type {Array<number|string>}
  362. * @default []
  363. * @requires modules/drag-panes
  364. */
  365. next: [],
  366. /**
  367. * Array of axes that should move with the current axis
  368. * while resizing.
  369. *
  370. * @sample {highstock} stock/yaxis/multiple-resizers
  371. * Three panes with resizers
  372. * @sample {highstock} stock/yaxis/resize-multiple-axes
  373. * One resizer controlling multiple axes
  374. *
  375. * @type {Array<number|string>}
  376. * @default []
  377. * @requires modules/drag-panes
  378. */
  379. prev: []
  380. },
  381. /**
  382. * Enable or disable resize by drag for the axis.
  383. *
  384. * @sample {highstock} stock/demo/candlestick-and-volume
  385. * Enabled resizer
  386. *
  387. * @requires modules/drag-panes
  388. */
  389. enabled: false,
  390. /**
  391. * Cursor style for the control line.
  392. *
  393. * In styled mode use class `highcharts-axis-resizer` instead.
  394. *
  395. * @requires modules/drag-panes
  396. */
  397. cursor: 'ns-resize',
  398. /**
  399. * Color of the control line.
  400. *
  401. * In styled mode use class `highcharts-axis-resizer` instead.
  402. *
  403. * @sample {highstock} stock/yaxis/styled-resizer
  404. * Styled resizer
  405. *
  406. * @type {Highcharts.ColorString}
  407. * @requires modules/drag-panes
  408. */
  409. lineColor: palette.neutralColor20,
  410. /**
  411. * Dash style of the control line.
  412. *
  413. * In styled mode use class `highcharts-axis-resizer` instead.
  414. *
  415. * @see For supported options check [dashStyle](#plotOptions.series.dashStyle)
  416. *
  417. * @sample {highstock} stock/yaxis/styled-resizer
  418. * Styled resizer
  419. *
  420. * @requires modules/drag-panes
  421. */
  422. lineDashStyle: 'Solid',
  423. /**
  424. * Width of the control line.
  425. *
  426. * In styled mode use class `highcharts-axis-resizer` instead.
  427. *
  428. * @sample {highstock} stock/yaxis/styled-resizer
  429. * Styled resizer
  430. *
  431. * @requires modules/drag-panes
  432. */
  433. lineWidth: 4,
  434. /**
  435. * Horizontal offset of the control line.
  436. *
  437. * @sample {highstock} stock/yaxis/styled-resizer
  438. * Styled resizer
  439. *
  440. * @requires modules/drag-panes
  441. */
  442. x: 0,
  443. /**
  444. * Vertical offset of the control line.
  445. *
  446. * @sample {highstock} stock/yaxis/styled-resizer
  447. * Styled resizer
  448. *
  449. * @requires modules/drag-panes
  450. */
  451. y: 0
  452. }
  453. };
  454. return AxisResizer;
  455. }());
  456. // Keep resizer reference on axis update
  457. Axis.keepProps.push('resizer');
  458. /* eslint-disable no-invalid-this */
  459. // Add new AxisResizer, update or remove it
  460. addEvent(Axis, 'afterRender', function () {
  461. var axis = this, resizer = axis.resizer, resizerOptions = axis.options.resize, enabled;
  462. if (resizerOptions) {
  463. enabled = resizerOptions.enabled !== false;
  464. if (resizer) {
  465. // Resizer present and enabled
  466. if (enabled) {
  467. // Update options
  468. resizer.init(axis, true);
  469. // Resizer present, but disabled
  470. }
  471. else {
  472. // Destroy the resizer
  473. resizer.destroy();
  474. }
  475. }
  476. else {
  477. // Resizer not present and enabled
  478. if (enabled) {
  479. // Add new resizer
  480. axis.resizer = new AxisResizer(axis);
  481. }
  482. // Resizer not present and disabled, so do nothing
  483. }
  484. }
  485. });
  486. // Clear resizer on axis remove.
  487. addEvent(Axis, 'destroy', function (e) {
  488. if (!e.keepEvents && this.resizer) {
  489. this.resizer.destroy();
  490. }
  491. });
  492. // Prevent any hover effects while dragging a control line of AxisResizer.
  493. wrap(Pointer.prototype, 'runPointActions', function (proceed) {
  494. if (!this.chart.activeResizer) {
  495. proceed.apply(this, Array.prototype.slice.call(arguments, 1));
  496. }
  497. });
  498. // Prevent default drag action detection while dragging a control line of
  499. // AxisResizer. (#7563)
  500. wrap(Pointer.prototype, 'drag', function (proceed) {
  501. if (!this.chart.activeResizer) {
  502. proceed.apply(this, Array.prototype.slice.call(arguments, 1));
  503. }
  504. });
  505. merge(true, Axis.defaultYAxisOptions, AxisResizer.resizerOptions);
  506. H.AxisResizer = AxisResizer;
  507. export default H.AxisResizer;