BoostOverrides.js 13 KB

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