BrokenAxis.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. /* *
  2. *
  3. * (c) 2009-2020 Torstein Honsi
  4. *
  5. * License: www.highcharts.com/license
  6. *
  7. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  8. *
  9. * */
  10. 'use strict';
  11. import Axis from './Axis.js';
  12. import H from '../Globals.js';
  13. import U from '../Utilities.js';
  14. var addEvent = U.addEvent, find = U.find, fireEvent = U.fireEvent, isArray = U.isArray, isNumber = U.isNumber, pick = U.pick;
  15. import '../Series/Series.js';
  16. import StackItem from '../../Extensions/Stacking.js';
  17. var Series = H.Series;
  18. /* eslint-disable valid-jsdoc */
  19. /**
  20. * Provides support for broken axes.
  21. * @private
  22. * @class
  23. */
  24. var BrokenAxisAdditions = /** @class */ (function () {
  25. /* *
  26. *
  27. * Constructors
  28. *
  29. * */
  30. function BrokenAxisAdditions(axis) {
  31. this.hasBreaks = false;
  32. this.axis = axis;
  33. }
  34. /* *
  35. *
  36. * Static Functions
  37. *
  38. * */
  39. /**
  40. * @private
  41. */
  42. BrokenAxisAdditions.isInBreak = function (brk, val) {
  43. var ret, repeat = brk.repeat || Infinity, from = brk.from, length = brk.to - brk.from, test = (val >= from ?
  44. (val - from) % repeat :
  45. repeat - ((from - val) % repeat));
  46. if (!brk.inclusive) {
  47. ret = test < length && test !== 0;
  48. }
  49. else {
  50. ret = test <= length;
  51. }
  52. return ret;
  53. };
  54. /**
  55. * @private
  56. */
  57. BrokenAxisAdditions.lin2Val = function (val) {
  58. var axis = this;
  59. var brokenAxis = axis.brokenAxis;
  60. var breakArray = brokenAxis && brokenAxis.breakArray;
  61. if (!breakArray) {
  62. return val;
  63. }
  64. var nval = val, brk, i;
  65. for (i = 0; i < breakArray.length; i++) {
  66. brk = breakArray[i];
  67. if (brk.from >= nval) {
  68. break;
  69. }
  70. else if (brk.to < nval) {
  71. nval += brk.len;
  72. }
  73. else if (BrokenAxisAdditions.isInBreak(brk, nval)) {
  74. nval += brk.len;
  75. }
  76. }
  77. return nval;
  78. };
  79. /**
  80. * @private
  81. */
  82. BrokenAxisAdditions.val2Lin = function (val) {
  83. var axis = this;
  84. var brokenAxis = axis.brokenAxis;
  85. var breakArray = brokenAxis && brokenAxis.breakArray;
  86. if (!breakArray) {
  87. return val;
  88. }
  89. var nval = val, brk, i;
  90. for (i = 0; i < breakArray.length; i++) {
  91. brk = breakArray[i];
  92. if (brk.to <= val) {
  93. nval -= brk.len;
  94. }
  95. else if (brk.from >= val) {
  96. break;
  97. }
  98. else if (BrokenAxisAdditions.isInBreak(brk, val)) {
  99. nval -= (val - brk.from);
  100. break;
  101. }
  102. }
  103. return nval;
  104. };
  105. /* *
  106. *
  107. * Functions
  108. *
  109. * */
  110. /**
  111. * Returns the first break found where the x is larger then break.from and
  112. * smaller then break.to.
  113. *
  114. * @param {number} x
  115. * The number which should be within a break.
  116. *
  117. * @param {Array<Highcharts.XAxisBreaksOptions>} breaks
  118. * The array of breaks to search within.
  119. *
  120. * @return {Highcharts.XAxisBreaksOptions|undefined}
  121. * Returns the first break found that matches, returns false if no break is
  122. * found.
  123. */
  124. BrokenAxisAdditions.prototype.findBreakAt = function (x, breaks) {
  125. return find(breaks, function (b) {
  126. return b.from < x && x < b.to;
  127. });
  128. };
  129. /**
  130. * @private
  131. */
  132. BrokenAxisAdditions.prototype.isInAnyBreak = function (val, testKeep) {
  133. var brokenAxis = this;
  134. var axis = brokenAxis.axis;
  135. var breaks = axis.options.breaks, i = breaks && breaks.length, inbrk, keep, ret;
  136. if (i) {
  137. while (i--) {
  138. if (BrokenAxisAdditions.isInBreak(breaks[i], val)) {
  139. inbrk = true;
  140. if (!keep) {
  141. keep = pick(breaks[i].showPoints, !axis.isXAxis);
  142. }
  143. }
  144. }
  145. if (inbrk && testKeep) {
  146. ret = inbrk && !keep;
  147. }
  148. else {
  149. ret = inbrk;
  150. }
  151. }
  152. return ret;
  153. };
  154. /**
  155. * Dynamically set or unset breaks in an axis. This function in lighter than
  156. * usin Axis.update, and it also preserves animation.
  157. *
  158. * @private
  159. * @function Highcharts.Axis#setBreaks
  160. *
  161. * @param {Array<Highcharts.XAxisBreaksOptions>} [breaks]
  162. * The breaks to add. When `undefined` it removes existing breaks.
  163. *
  164. * @param {boolean} [redraw=true]
  165. * Whether to redraw the chart immediately.
  166. *
  167. * @return {void}
  168. */
  169. BrokenAxisAdditions.prototype.setBreaks = function (breaks, redraw) {
  170. var brokenAxis = this;
  171. var axis = brokenAxis.axis;
  172. var hasBreaks = (isArray(breaks) && !!breaks.length);
  173. axis.isDirty = brokenAxis.hasBreaks !== hasBreaks;
  174. brokenAxis.hasBreaks = hasBreaks;
  175. axis.options.breaks = axis.userOptions.breaks = breaks;
  176. axis.forceRedraw = true; // Force recalculation in setScale
  177. // Recalculate series related to the axis.
  178. axis.series.forEach(function (series) {
  179. series.isDirty = true;
  180. });
  181. if (!hasBreaks && axis.val2lin === BrokenAxisAdditions.val2Lin) {
  182. // Revert to prototype functions
  183. delete axis.val2lin;
  184. delete axis.lin2val;
  185. }
  186. if (hasBreaks) {
  187. axis.userOptions.ordinal = false;
  188. axis.lin2val = BrokenAxisAdditions.lin2Val;
  189. axis.val2lin = BrokenAxisAdditions.val2Lin;
  190. axis.setExtremes = function (newMin, newMax, redraw, animation, eventArguments) {
  191. // If trying to set extremes inside a break, extend min to
  192. // after, and max to before the break ( #3857 )
  193. if (brokenAxis.hasBreaks) {
  194. var axisBreak, breaks = this.options.breaks;
  195. while ((axisBreak = brokenAxis.findBreakAt(newMin, breaks))) {
  196. newMin = axisBreak.to;
  197. }
  198. while ((axisBreak = brokenAxis.findBreakAt(newMax, breaks))) {
  199. newMax = axisBreak.from;
  200. }
  201. // If both min and max is within the same break.
  202. if (newMax < newMin) {
  203. newMax = newMin;
  204. }
  205. }
  206. Axis.prototype.setExtremes.call(this, newMin, newMax, redraw, animation, eventArguments);
  207. };
  208. axis.setAxisTranslation = function (saveOld) {
  209. Axis.prototype.setAxisTranslation.call(this, saveOld);
  210. brokenAxis.unitLength = null;
  211. if (brokenAxis.hasBreaks) {
  212. var breaks = axis.options.breaks || [],
  213. // Temporary one:
  214. breakArrayT = [], breakArray = [], length = 0, inBrk, repeat, min = axis.userMin || axis.min, max = axis.userMax || axis.max, pointRangePadding = pick(axis.pointRangePadding, 0), start, i;
  215. // Min & max check (#4247)
  216. breaks.forEach(function (brk) {
  217. repeat = brk.repeat || Infinity;
  218. if (BrokenAxisAdditions.isInBreak(brk, min)) {
  219. min +=
  220. (brk.to % repeat) -
  221. (min % repeat);
  222. }
  223. if (BrokenAxisAdditions.isInBreak(brk, max)) {
  224. max -=
  225. (max % repeat) -
  226. (brk.from % repeat);
  227. }
  228. });
  229. // Construct an array holding all breaks in the axis
  230. breaks.forEach(function (brk) {
  231. start = brk.from;
  232. repeat = brk.repeat || Infinity;
  233. while (start - repeat > min) {
  234. start -= repeat;
  235. }
  236. while (start < min) {
  237. start += repeat;
  238. }
  239. for (i = start; i < max; i += repeat) {
  240. breakArrayT.push({
  241. value: i,
  242. move: 'in'
  243. });
  244. breakArrayT.push({
  245. value: i + (brk.to - brk.from),
  246. move: 'out',
  247. size: brk.breakSize
  248. });
  249. }
  250. });
  251. breakArrayT.sort(function (a, b) {
  252. return ((a.value === b.value) ?
  253. ((a.move === 'in' ? 0 : 1) -
  254. (b.move === 'in' ? 0 : 1)) :
  255. a.value - b.value);
  256. });
  257. // Simplify the breaks
  258. inBrk = 0;
  259. start = min;
  260. breakArrayT.forEach(function (brk) {
  261. inBrk += (brk.move === 'in' ? 1 : -1);
  262. if (inBrk === 1 && brk.move === 'in') {
  263. start = brk.value;
  264. }
  265. if (inBrk === 0) {
  266. breakArray.push({
  267. from: start,
  268. to: brk.value,
  269. len: brk.value - start - (brk.size || 0)
  270. });
  271. length += brk.value - start - (brk.size || 0);
  272. }
  273. });
  274. /**
  275. * HC <= 8 backwards compatibility, used by demo samples.
  276. * @deprecated
  277. * @private
  278. * @requires modules/broken-axis
  279. */
  280. axis.breakArray = brokenAxis.breakArray = breakArray;
  281. // Used with staticScale, and below the actual axis length,
  282. // when breaks are substracted.
  283. brokenAxis.unitLength = max - min - length + pointRangePadding;
  284. fireEvent(axis, 'afterBreaks');
  285. if (axis.staticScale) {
  286. axis.transA = axis.staticScale;
  287. }
  288. else if (brokenAxis.unitLength) {
  289. axis.transA *=
  290. (max - axis.min + pointRangePadding) /
  291. brokenAxis.unitLength;
  292. }
  293. if (pointRangePadding) {
  294. axis.minPixelPadding =
  295. axis.transA * axis.minPointOffset;
  296. }
  297. axis.min = min;
  298. axis.max = max;
  299. }
  300. };
  301. }
  302. if (pick(redraw, true)) {
  303. axis.chart.redraw();
  304. }
  305. };
  306. return BrokenAxisAdditions;
  307. }());
  308. /**
  309. * Axis with support of broken data rows.
  310. * @private
  311. * @class
  312. */
  313. var BrokenAxis = /** @class */ (function () {
  314. function BrokenAxis() {
  315. }
  316. /**
  317. * Adds support for broken axes.
  318. * @private
  319. */
  320. BrokenAxis.compose = function (AxisClass, SeriesClass) {
  321. AxisClass.keepProps.push('brokenAxis');
  322. var seriesProto = Series.prototype;
  323. /**
  324. * @private
  325. */
  326. seriesProto.drawBreaks = function (axis, keys) {
  327. var series = this, points = series.points, breaks, threshold, eventName, y;
  328. if (axis && // #5950
  329. axis.brokenAxis &&
  330. axis.brokenAxis.hasBreaks) {
  331. var brokenAxis_1 = axis.brokenAxis;
  332. keys.forEach(function (key) {
  333. breaks = brokenAxis_1 && brokenAxis_1.breakArray || [];
  334. threshold = axis.isXAxis ?
  335. axis.min :
  336. pick(series.options.threshold, axis.min);
  337. points.forEach(function (point) {
  338. y = pick(point['stack' + key.toUpperCase()], point[key]);
  339. breaks.forEach(function (brk) {
  340. if (isNumber(threshold) && isNumber(y)) {
  341. eventName = false;
  342. if ((threshold < brk.from && y > brk.to) ||
  343. (threshold > brk.from && y < brk.from)) {
  344. eventName = 'pointBreak';
  345. }
  346. else if ((threshold < brk.from && y > brk.from && y < brk.to) ||
  347. (threshold > brk.from && y > brk.to && y < brk.from)) {
  348. eventName = 'pointInBreak';
  349. }
  350. if (eventName) {
  351. fireEvent(axis, eventName, { point: point, brk: brk });
  352. }
  353. }
  354. });
  355. });
  356. });
  357. }
  358. };
  359. /**
  360. * Extend getGraphPath by identifying gaps in the data so that we can
  361. * draw a gap in the line or area. This was moved from ordinal axis
  362. * module to broken axis module as of #5045.
  363. *
  364. * @private
  365. * @function Highcharts.Series#gappedPath
  366. *
  367. * @return {Highcharts.SVGPathArray}
  368. * Gapped path
  369. */
  370. seriesProto.gappedPath = function () {
  371. var currentDataGrouping = this.currentDataGrouping, groupingSize = currentDataGrouping && currentDataGrouping.gapSize, gapSize = this.options.gapSize, points = this.points.slice(), i = points.length - 1, yAxis = this.yAxis, stack;
  372. /**
  373. * Defines when to display a gap in the graph, together with the
  374. * [gapUnit](plotOptions.series.gapUnit) option.
  375. *
  376. * In case when `dataGrouping` is enabled, points can be grouped
  377. * into a larger time span. This can make the grouped points to have
  378. * a greater distance than the absolute value of `gapSize` property,
  379. * which will result in disappearing graph completely. To prevent
  380. * this situation the mentioned distance between grouped points is
  381. * used instead of previously defined `gapSize`.
  382. *
  383. * In practice, this option is most often used to visualize gaps in
  384. * time series. In a stock chart, intraday data is available for
  385. * daytime hours, while gaps will appear in nights and weekends.
  386. *
  387. * @see [gapUnit](plotOptions.series.gapUnit)
  388. * @see [xAxis.breaks](#xAxis.breaks)
  389. *
  390. * @sample {highstock} stock/plotoptions/series-gapsize/
  391. * Setting the gap size to 2 introduces gaps for weekends
  392. * in daily datasets.
  393. *
  394. * @type {number}
  395. * @default 0
  396. * @product highstock
  397. * @requires modules/broken-axis
  398. * @apioption plotOptions.series.gapSize
  399. */
  400. /**
  401. * Together with [gapSize](plotOptions.series.gapSize), this option
  402. * defines where to draw gaps in the graph.
  403. *
  404. * When the `gapUnit` is `"relative"` (default), a gap size of 5
  405. * means that if the distance between two points is greater than
  406. * 5 times that of the two closest points, the graph will be broken.
  407. *
  408. * When the `gapUnit` is `"value"`, the gap is based on absolute
  409. * axis values, which on a datetime axis is milliseconds. This also
  410. * applies to the navigator series that inherits gap options from
  411. * the base series.
  412. *
  413. * @see [gapSize](plotOptions.series.gapSize)
  414. *
  415. * @type {string}
  416. * @default relative
  417. * @since 5.0.13
  418. * @product highstock
  419. * @validvalue ["relative", "value"]
  420. * @requires modules/broken-axis
  421. * @apioption plotOptions.series.gapUnit
  422. */
  423. if (gapSize && i > 0) { // #5008
  424. // Gap unit is relative
  425. if (this.options.gapUnit !== 'value') {
  426. gapSize *= this.basePointRange;
  427. }
  428. // Setting a new gapSize in case dataGrouping is enabled (#7686)
  429. if (groupingSize &&
  430. groupingSize > gapSize &&
  431. // Except when DG is forced (e.g. from other series)
  432. // and has lower granularity than actual points (#11351)
  433. groupingSize >= this.basePointRange) {
  434. gapSize = groupingSize;
  435. }
  436. // extension for ordinal breaks
  437. var current = void 0, next = void 0;
  438. while (i--) {
  439. // Reassign next if it is not visible
  440. if (!(next && next.visible !== false)) {
  441. next = points[i + 1];
  442. }
  443. current = points[i];
  444. // Skip iteration if one of the points is not visible
  445. if (next.visible === false || current.visible === false) {
  446. continue;
  447. }
  448. if (next.x - current.x > gapSize) {
  449. var xRange = (current.x + next.x) / 2;
  450. points.splice(// insert after this one
  451. i + 1, 0, {
  452. isNull: true,
  453. x: xRange
  454. });
  455. // For stacked chart generate empty stack items, #6546
  456. if (yAxis.stacking && this.options.stacking) {
  457. stack = yAxis.stacking.stacks[this.stackKey][xRange] =
  458. new StackItem(yAxis, yAxis.options
  459. .stackLabels, false, xRange, this.stack);
  460. stack.total = 0;
  461. }
  462. }
  463. // Assign current to next for the upcoming iteration
  464. next = current;
  465. }
  466. }
  467. // Call base method
  468. return this.getGraphPath(points);
  469. };
  470. /* eslint-disable no-invalid-this */
  471. addEvent(AxisClass, 'init', function () {
  472. var axis = this;
  473. if (!axis.brokenAxis) {
  474. axis.brokenAxis = new BrokenAxisAdditions(axis);
  475. }
  476. });
  477. addEvent(AxisClass, 'afterInit', function () {
  478. if (typeof this.brokenAxis !== 'undefined') {
  479. this.brokenAxis.setBreaks(this.options.breaks, false);
  480. }
  481. });
  482. addEvent(AxisClass, 'afterSetTickPositions', function () {
  483. var axis = this;
  484. var brokenAxis = axis.brokenAxis;
  485. if (brokenAxis &&
  486. brokenAxis.hasBreaks) {
  487. var tickPositions = this.tickPositions, info = this.tickPositions.info, newPositions = [], i;
  488. for (i = 0; i < tickPositions.length; i++) {
  489. if (!brokenAxis.isInAnyBreak(tickPositions[i])) {
  490. newPositions.push(tickPositions[i]);
  491. }
  492. }
  493. this.tickPositions = newPositions;
  494. this.tickPositions.info = info;
  495. }
  496. });
  497. // Force Axis to be not-ordinal when breaks are defined
  498. addEvent(AxisClass, 'afterSetOptions', function () {
  499. if (this.brokenAxis && this.brokenAxis.hasBreaks) {
  500. this.options.ordinal = false;
  501. }
  502. });
  503. addEvent(SeriesClass, 'afterGeneratePoints', function () {
  504. var _a = this, isDirty = _a.isDirty, connectNulls = _a.options.connectNulls, points = _a.points, xAxis = _a.xAxis, yAxis = _a.yAxis;
  505. // Set, or reset visibility of the points. Axis.setBreaks marks the
  506. // series as isDirty
  507. if (isDirty) {
  508. var i = points.length;
  509. while (i--) {
  510. var point = points[i];
  511. // Respect nulls inside the break (#4275)
  512. var nullGap = point.y === null && connectNulls === false;
  513. var isPointInBreak = (!nullGap && ((xAxis &&
  514. xAxis.brokenAxis &&
  515. xAxis.brokenAxis.isInAnyBreak(point.x, true)) || (yAxis &&
  516. yAxis.brokenAxis &&
  517. yAxis.brokenAxis.isInAnyBreak(point.y, true))));
  518. // Set point.visible if in any break.
  519. // If not in break, reset visible to original value.
  520. point.visible = isPointInBreak ?
  521. false :
  522. point.options.visible !== false;
  523. }
  524. }
  525. });
  526. addEvent(SeriesClass, 'afterRender', function drawPointsWrapped() {
  527. this.drawBreaks(this.xAxis, ['x']);
  528. this.drawBreaks(this.yAxis, pick(this.pointArrayMap, ['y']));
  529. });
  530. };
  531. return BrokenAxis;
  532. }());
  533. BrokenAxis.compose(Axis, Series); // @todo remove automatism
  534. export default BrokenAxis;