BoostOverrides.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. /* *
  2. *
  3. * Copyright (c) 2019-2020 Highsoft AS
  4. *
  5. * Boost module: stripped-down renderer for higher performance
  6. *
  7. * License: highcharts.com/license
  8. *
  9. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  10. *
  11. * */
  12. 'use strict';
  13. import Chart from '../../Core/Chart/Chart.js';
  14. import H from '../../Core/Globals.js';
  15. import Point from '../../Core/Series/Point.js';
  16. import U from '../../Core/Utilities.js';
  17. var addEvent = U.addEvent, error = U.error, getOptions = U.getOptions, isArray = U.isArray, isNumber = U.isNumber, pick = U.pick, wrap = U.wrap;
  18. import '../../Core/Series/Series.js';
  19. import '../../Core/Options.js';
  20. import '../../Core/Interaction.js';
  21. import butils from './BoostUtils.js';
  22. import boostable from './Boostables.js';
  23. import boostableMap from './BoostableMap.js';
  24. var boostEnabled = butils.boostEnabled, shouldForceChartSeriesBoosting = butils.shouldForceChartSeriesBoosting, Series = H.Series, seriesTypes = H.seriesTypes, plotOptions = getOptions().plotOptions;
  25. /**
  26. * Returns true if the chart is in series boost mode.
  27. *
  28. * @function Highcharts.Chart#isChartSeriesBoosting
  29. *
  30. * @param {Highcharts.Chart} chart
  31. * the chart to check
  32. *
  33. * @return {boolean}
  34. * true if the chart is in series boost mode
  35. */
  36. Chart.prototype.isChartSeriesBoosting = function () {
  37. var isSeriesBoosting, threshold = pick(this.options.boost && this.options.boost.seriesThreshold, 50);
  38. isSeriesBoosting = threshold <= this.series.length ||
  39. shouldForceChartSeriesBoosting(this);
  40. return isSeriesBoosting;
  41. };
  42. /* eslint-disable valid-jsdoc */
  43. /**
  44. * Get the clip rectangle for a target, either a series or the chart. For the
  45. * chart, we need to consider the maximum extent of its Y axes, in case of
  46. * Highstock panes and navigator.
  47. *
  48. * @private
  49. * @function Highcharts.Chart#getBoostClipRect
  50. *
  51. * @param {Highcharts.Chart} target
  52. *
  53. * @return {Highcharts.BBoxObject}
  54. */
  55. Chart.prototype.getBoostClipRect = function (target) {
  56. var clipBox = {
  57. x: this.plotLeft,
  58. y: this.plotTop,
  59. width: this.plotWidth,
  60. height: this.plotHeight
  61. };
  62. if (target === this) {
  63. this.yAxis.forEach(function (yAxis) {
  64. clipBox.y = Math.min(yAxis.pos, clipBox.y);
  65. clipBox.height = Math.max(yAxis.pos - this.plotTop + yAxis.len, clipBox.height);
  66. }, this);
  67. }
  68. return clipBox;
  69. };
  70. /**
  71. * Return a full Point object based on the index.
  72. * The boost module uses stripped point objects for performance reasons.
  73. *
  74. * @function Highcharts.Series#getPoint
  75. *
  76. * @param {object|Highcharts.Point} boostPoint
  77. * A stripped-down point object
  78. *
  79. * @return {Highcharts.Point}
  80. * A Point object as per https://api.highcharts.com/highcharts#Point
  81. */
  82. Series.prototype.getPoint = function (boostPoint) {
  83. var point = boostPoint, xData = (this.xData || this.options.xData || this.processedXData ||
  84. false);
  85. if (boostPoint && !(boostPoint instanceof this.pointClass)) {
  86. point = (new this.pointClass()).init(// eslint-disable-line new-cap
  87. this, this.options.data[boostPoint.i], xData ? xData[boostPoint.i] : void 0);
  88. point.category = pick(this.xAxis.categories ?
  89. this.xAxis.categories[point.x] :
  90. point.x, // @todo simplify
  91. point.x);
  92. point.dist = boostPoint.dist;
  93. point.distX = boostPoint.distX;
  94. point.plotX = boostPoint.plotX;
  95. point.plotY = boostPoint.plotY;
  96. point.index = boostPoint.i;
  97. point.isInside = this.isPointInside(boostPoint);
  98. }
  99. return point;
  100. };
  101. /* eslint-disable no-invalid-this */
  102. // Return a point instance from the k-d-tree
  103. wrap(Series.prototype, 'searchPoint', function (proceed) {
  104. return this.getPoint(proceed.apply(this, [].slice.call(arguments, 1)));
  105. });
  106. // For inverted series, we need to swap X-Y values before running base methods
  107. wrap(Point.prototype, 'haloPath', function (proceed) {
  108. var halo, point = this, series = point.series, chart = series.chart, plotX = point.plotX, plotY = point.plotY, inverted = chart.inverted;
  109. if (series.isSeriesBoosting && inverted) {
  110. point.plotX = series.yAxis.len - plotY;
  111. point.plotY = series.xAxis.len - plotX;
  112. }
  113. halo = proceed.apply(this, Array.prototype.slice.call(arguments, 1));
  114. if (series.isSeriesBoosting && inverted) {
  115. point.plotX = plotX;
  116. point.plotY = plotY;
  117. }
  118. return halo;
  119. });
  120. wrap(Series.prototype, 'markerAttribs', function (proceed, point) {
  121. var attribs, series = this, chart = series.chart, plotX = point.plotX, plotY = point.plotY, inverted = chart.inverted;
  122. if (series.isSeriesBoosting && inverted) {
  123. point.plotX = series.yAxis.len - plotY;
  124. point.plotY = series.xAxis.len - plotX;
  125. }
  126. attribs = proceed.apply(this, Array.prototype.slice.call(arguments, 1));
  127. if (series.isSeriesBoosting && inverted) {
  128. point.plotX = plotX;
  129. point.plotY = plotY;
  130. }
  131. return attribs;
  132. });
  133. /*
  134. * Extend series.destroy to also remove the fake k-d-tree points (#5137).
  135. * Normally this is handled by Series.destroy that calls Point.destroy,
  136. * but the fake search points are not registered like that.
  137. */
  138. addEvent(Series, 'destroy', function () {
  139. var series = this, chart = series.chart;
  140. if (chart.markerGroup === series.markerGroup) {
  141. series.markerGroup = null;
  142. }
  143. if (chart.hoverPoints) {
  144. chart.hoverPoints = chart.hoverPoints.filter(function (point) {
  145. return point.series === series;
  146. });
  147. }
  148. if (chart.hoverPoint && chart.hoverPoint.series === series) {
  149. chart.hoverPoint = null;
  150. }
  151. });
  152. /*
  153. * Do not compute extremes when min and max are set.
  154. * If we use this in the core, we can add the hook
  155. * to hasExtremes to the methods directly.
  156. */
  157. wrap(Series.prototype, 'getExtremes', function (proceed) {
  158. if (!this.isSeriesBoosting || (!this.hasExtremes || !this.hasExtremes())) {
  159. return proceed.apply(this, Array.prototype.slice.call(arguments, 1));
  160. }
  161. return {};
  162. });
  163. /*
  164. * Override a bunch of methods the same way. If the number of points is
  165. * below the threshold, run the original method. If not, check for a
  166. * canvas version or do nothing.
  167. *
  168. * Note that we're not overriding any of these for heatmaps.
  169. */
  170. [
  171. 'translate',
  172. 'generatePoints',
  173. 'drawTracker',
  174. 'drawPoints',
  175. 'render'
  176. ].forEach(function (method) {
  177. /**
  178. * @private
  179. */
  180. function branch(proceed) {
  181. var letItPass = this.options.stacking &&
  182. (method === 'translate' || method === 'generatePoints');
  183. if (!this.isSeriesBoosting ||
  184. letItPass ||
  185. !boostEnabled(this.chart) ||
  186. this.type === 'heatmap' ||
  187. this.type === 'treemap' ||
  188. !boostableMap[this.type] ||
  189. this.options.boostThreshold === 0) {
  190. proceed.call(this);
  191. // If a canvas version of the method exists, like renderCanvas(), run
  192. }
  193. else if (this[method + 'Canvas']) {
  194. this[method + 'Canvas']();
  195. }
  196. }
  197. wrap(Series.prototype, method, branch);
  198. // A special case for some types - their translate method is already wrapped
  199. if (method === 'translate') {
  200. [
  201. 'column',
  202. 'bar',
  203. 'arearange',
  204. 'columnrange',
  205. 'heatmap',
  206. 'treemap'
  207. ].forEach(function (type) {
  208. if (seriesTypes[type]) {
  209. wrap(seriesTypes[type].prototype, method, branch);
  210. }
  211. });
  212. }
  213. });
  214. // If the series is a heatmap or treemap, or if the series is not boosting
  215. // do the default behaviour. Otherwise, process if the series has no extremes.
  216. wrap(Series.prototype, 'processData', function (proceed) {
  217. var series = this, dataToMeasure = this.options.data, firstPoint;
  218. /**
  219. * Used twice in this function, first on this.options.data, the second
  220. * time it runs the check again after processedXData is built.
  221. * @private
  222. * @todo Check what happens with data grouping
  223. */
  224. function getSeriesBoosting(data) {
  225. return series.chart.isChartSeriesBoosting() || ((data ? data.length : 0) >=
  226. (series.options.boostThreshold || Number.MAX_VALUE));
  227. }
  228. if (boostEnabled(this.chart) && boostableMap[this.type]) {
  229. // If there are no extremes given in the options, we also need to
  230. // process the data to read the data extremes. If this is a heatmap, do
  231. // default behaviour.
  232. if (!getSeriesBoosting(dataToMeasure) || // First pass with options.data
  233. this.type === 'heatmap' ||
  234. this.type === 'treemap' ||
  235. this.options.stacking || // processedYData for the stack (#7481)
  236. !this.hasExtremes ||
  237. !this.hasExtremes(true)) {
  238. proceed.apply(this, Array.prototype.slice.call(arguments, 1));
  239. dataToMeasure = this.processedXData;
  240. }
  241. // Set the isBoosting flag, second pass with processedXData to see if we
  242. // have zoomed.
  243. this.isSeriesBoosting = getSeriesBoosting(dataToMeasure);
  244. // Enter or exit boost mode
  245. if (this.isSeriesBoosting) {
  246. // Force turbo-mode:
  247. firstPoint = this.getFirstValidPoint(this.options.data);
  248. if (!isNumber(firstPoint) && !isArray(firstPoint)) {
  249. error(12, false, this.chart);
  250. }
  251. this.enterBoost();
  252. }
  253. else if (this.exitBoost) {
  254. this.exitBoost();
  255. }
  256. // The series type is not boostable
  257. }
  258. else {
  259. proceed.apply(this, Array.prototype.slice.call(arguments, 1));
  260. }
  261. });
  262. addEvent(Series, 'hide', function () {
  263. if (this.canvas && this.renderTarget) {
  264. if (this.ogl) {
  265. this.ogl.clear();
  266. }
  267. this.boostClear();
  268. }
  269. });
  270. /**
  271. * Enter boost mode and apply boost-specific properties.
  272. *
  273. * @function Highcharts.Series#enterBoost
  274. */
  275. Series.prototype.enterBoost = function () {
  276. this.alteredByBoost = [];
  277. // Save the original values, including whether it was an own property or
  278. // inherited from the prototype.
  279. ['allowDG', 'directTouch', 'stickyTracking'].forEach(function (prop) {
  280. this.alteredByBoost.push({
  281. prop: prop,
  282. val: this[prop],
  283. own: Object.hasOwnProperty.call(this, prop)
  284. });
  285. }, this);
  286. this.allowDG = false;
  287. this.directTouch = false;
  288. this.stickyTracking = true;
  289. // Prevent animation when zooming in on boosted series(#13421).
  290. this.finishedAnimating = true;
  291. // Hide series label if any
  292. if (this.labelBySeries) {
  293. this.labelBySeries = this.labelBySeries.destroy();
  294. }
  295. };
  296. /**
  297. * Exit from boost mode and restore non-boost properties.
  298. *
  299. * @function Highcharts.Series#exitBoost
  300. */
  301. Series.prototype.exitBoost = function () {
  302. // Reset instance properties and/or delete instance properties and go back
  303. // to prototype
  304. (this.alteredByBoost || []).forEach(function (setting) {
  305. if (setting.own) {
  306. this[setting.prop] = setting.val;
  307. }
  308. else {
  309. // Revert to prototype
  310. delete this[setting.prop];
  311. }
  312. }, this);
  313. // Clear previous run
  314. if (this.boostClear) {
  315. this.boostClear();
  316. }
  317. };
  318. /**
  319. * @private
  320. * @function Highcharts.Series#hasExtremes
  321. *
  322. * @param {boolean} checkX
  323. *
  324. * @return {boolean}
  325. */
  326. Series.prototype.hasExtremes = function (checkX) {
  327. var options = this.options, data = options.data, xAxis = this.xAxis && this.xAxis.options, yAxis = this.yAxis && this.yAxis.options, colorAxis = this.colorAxis && this.colorAxis.options;
  328. return data.length > (options.boostThreshold || Number.MAX_VALUE) &&
  329. // Defined yAxis extremes
  330. isNumber(yAxis.min) &&
  331. isNumber(yAxis.max) &&
  332. // Defined (and required) xAxis extremes
  333. (!checkX ||
  334. (isNumber(xAxis.min) && isNumber(xAxis.max))) &&
  335. // Defined (e.g. heatmap) colorAxis extremes
  336. (!colorAxis ||
  337. (isNumber(colorAxis.min) && isNumber(colorAxis.max)));
  338. };
  339. /**
  340. * If implemented in the core, parts of this can probably be
  341. * shared with other similar methods in Highcharts.
  342. *
  343. * @function Highcharts.Series#destroyGraphics
  344. */
  345. Series.prototype.destroyGraphics = function () {
  346. var series = this, points = this.points, point, i;
  347. if (points) {
  348. for (i = 0; i < points.length; i = i + 1) {
  349. point = points[i];
  350. if (point && point.destroyElements) {
  351. point.destroyElements(); // #7557
  352. }
  353. }
  354. }
  355. ['graph', 'area', 'tracker'].forEach(function (prop) {
  356. if (series[prop]) {
  357. series[prop] = series[prop].destroy();
  358. }
  359. });
  360. };
  361. // Set default options
  362. boostable.forEach(function (type) {
  363. if (plotOptions[type]) {
  364. plotOptions[type].boostThreshold = 5000;
  365. plotOptions[type].boostData = [];
  366. seriesTypes[type].prototype.fillOpacity = true;
  367. }
  368. });