BoostInit.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  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 H from '../../Core/Globals.js';
  15. var noop = H.noop;
  16. import Series from '../../Core/Series/Series.js';
  17. import SeriesRegistry from '../../Core/Series/SeriesRegistry.js';
  18. var seriesTypes = SeriesRegistry.seriesTypes;
  19. import U from '../../Core/Utilities.js';
  20. var addEvent = U.addEvent, extend = U.extend, fireEvent = U.fireEvent, wrap = U.wrap;
  21. import butils from './BoostUtils.js';
  22. import createAndAttachRenderer from './BoostAttach.js';
  23. var eachAsync = butils.eachAsync, pointDrawHandler = butils.pointDrawHandler, allocateIfNotSeriesBoosting = butils.allocateIfNotSeriesBoosting, renderIfNotSeriesBoosting = butils.renderIfNotSeriesBoosting, shouldForceChartSeriesBoosting = butils.shouldForceChartSeriesBoosting, index;
  24. /* eslint-disable valid-jsdoc */
  25. /**
  26. * Initialize the boot module.
  27. *
  28. * @private
  29. * @return {void}
  30. */
  31. function init() {
  32. extend(Series.prototype, {
  33. /**
  34. * @private
  35. * @function Highcharts.Series#renderCanvas
  36. */
  37. renderCanvas: function () {
  38. var series = this, options = series.options || {}, renderer = false, chart = series.chart, xAxis = this.xAxis, yAxis = this.yAxis, xData = options.xData || series.processedXData, yData = options.yData || series.processedYData, rawData = options.data, xExtremes = xAxis.getExtremes(), xMin = xExtremes.min, xMax = xExtremes.max, yExtremes = yAxis.getExtremes(), yMin = yExtremes.min, yMax = yExtremes.max, pointTaken = {}, lastClientX, sampling = !!series.sampling, points, enableMouseTracking = options.enableMouseTracking !== false, threshold = options.threshold, yBottom = yAxis.getThreshold(threshold), isRange = series.pointArrayMap &&
  39. series.pointArrayMap.join(',') === 'low,high', isStacked = !!options.stacking, cropStart = series.cropStart || 0, requireSorting = series.requireSorting, useRaw = !xData, minVal, maxVal, minI, maxI, boostOptions, compareX = options.findNearestPointBy === 'x', xDataFull = (this.xData ||
  40. this.options.xData ||
  41. this.processedXData ||
  42. false), addKDPoint = function (clientX, plotY, i) {
  43. // We need to do ceil on the clientX to make things
  44. // snap to pixel values. The renderer will frequently
  45. // draw stuff on "sub-pixels".
  46. clientX = Math.ceil(clientX);
  47. // Shaves off about 60ms compared to repeated concatenation
  48. index = compareX ? clientX : clientX + ',' + plotY;
  49. // The k-d tree requires series points.
  50. // Reduce the amount of points, since the time to build the
  51. // tree increases exponentially.
  52. if (enableMouseTracking && !pointTaken[index]) {
  53. pointTaken[index] = true;
  54. if (chart.inverted) {
  55. clientX = xAxis.len - clientX;
  56. plotY = yAxis.len - plotY;
  57. }
  58. points.push({
  59. x: xDataFull ? xDataFull[cropStart + i] : false,
  60. clientX: clientX,
  61. plotX: clientX,
  62. plotY: plotY,
  63. i: cropStart + i
  64. });
  65. }
  66. };
  67. // Get or create the renderer
  68. renderer = createAndAttachRenderer(chart, series);
  69. chart.isBoosting = true;
  70. boostOptions = renderer.settings;
  71. if (!this.visible) {
  72. return;
  73. }
  74. // If we are zooming out from SVG mode, destroy the graphics
  75. if (this.points || this.graph) {
  76. this.destroyGraphics();
  77. }
  78. // If we're rendering per. series we should create the marker groups
  79. // as usual.
  80. if (!chart.isChartSeriesBoosting()) {
  81. // If all series were boosting, but are not anymore
  82. // restore private markerGroup
  83. if (this.markerGroup === chart.markerGroup) {
  84. this.markerGroup = void 0;
  85. }
  86. this.markerGroup = series.plotGroup('markerGroup', 'markers', true, 1, chart.seriesGroup);
  87. }
  88. else {
  89. // If series has a private markeGroup, remove that
  90. // and use common markerGroup
  91. if (this.markerGroup &&
  92. this.markerGroup !== chart.markerGroup) {
  93. this.markerGroup.destroy();
  94. }
  95. // Use a single group for the markers
  96. this.markerGroup = chart.markerGroup;
  97. // When switching from chart boosting mode, destroy redundant
  98. // series boosting targets
  99. if (this.renderTarget) {
  100. this.renderTarget = this.renderTarget.destroy();
  101. }
  102. }
  103. points = this.points = [];
  104. // Do not start building while drawing
  105. series.buildKDTree = noop;
  106. if (renderer) {
  107. allocateIfNotSeriesBoosting(renderer, this);
  108. renderer.pushSeries(series);
  109. // Perform the actual renderer if we're on series level
  110. renderIfNotSeriesBoosting(renderer, this, chart);
  111. }
  112. /**
  113. * This builds the KD-tree
  114. * @private
  115. */
  116. function processPoint(d, i) {
  117. var x, y, clientX, plotY, isNull, low = false, chartDestroyed = typeof chart.index === 'undefined', isYInside = true;
  118. if (typeof d === 'undefined') {
  119. return true;
  120. }
  121. if (!chartDestroyed) {
  122. if (useRaw) {
  123. x = d[0];
  124. y = d[1];
  125. }
  126. else {
  127. x = d;
  128. y = yData[i];
  129. }
  130. // Resolve low and high for range series
  131. if (isRange) {
  132. if (useRaw) {
  133. y = d.slice(1, 3);
  134. }
  135. low = y[0];
  136. y = y[1];
  137. }
  138. else if (isStacked) {
  139. x = d.x;
  140. y = d.stackY;
  141. low = y - d.y;
  142. }
  143. isNull = y === null;
  144. // Optimize for scatter zooming
  145. if (!requireSorting) {
  146. isYInside = y >= yMin && y <= yMax;
  147. }
  148. if (!isNull && x >= xMin && x <= xMax && isYInside) {
  149. clientX = xAxis.toPixels(x, true);
  150. if (sampling) {
  151. if (typeof minI === 'undefined' ||
  152. clientX === lastClientX) {
  153. if (!isRange) {
  154. low = y;
  155. }
  156. if (typeof maxI === 'undefined' ||
  157. y > maxVal) {
  158. maxVal = y;
  159. maxI = i;
  160. }
  161. if (typeof minI === 'undefined' ||
  162. low < minVal) {
  163. minVal = low;
  164. minI = i;
  165. }
  166. }
  167. // Add points and reset
  168. if (clientX !== lastClientX) {
  169. // maxI is number too:
  170. if (typeof minI !== 'undefined') {
  171. plotY =
  172. yAxis.toPixels(maxVal, true);
  173. yBottom =
  174. yAxis.toPixels(minVal, true);
  175. addKDPoint(clientX, plotY, maxI);
  176. if (yBottom !== plotY) {
  177. addKDPoint(clientX, yBottom, minI);
  178. }
  179. }
  180. minI = maxI = void 0;
  181. lastClientX = clientX;
  182. }
  183. }
  184. else {
  185. plotY = Math.ceil(yAxis.toPixels(y, true));
  186. addKDPoint(clientX, plotY, i);
  187. }
  188. }
  189. }
  190. return !chartDestroyed;
  191. }
  192. /**
  193. * @private
  194. */
  195. function doneProcessing() {
  196. fireEvent(series, 'renderedCanvas');
  197. // Go back to prototype, ready to build
  198. delete series.buildKDTree;
  199. series.buildKDTree();
  200. if (boostOptions.debug.timeKDTree) {
  201. console.timeEnd('kd tree building'); // eslint-disable-line no-console
  202. }
  203. }
  204. // Loop over the points to build the k-d tree - skip this if
  205. // exporting
  206. if (!chart.renderer.forExport) {
  207. if (boostOptions.debug.timeKDTree) {
  208. console.time('kd tree building'); // eslint-disable-line no-console
  209. }
  210. eachAsync(isStacked ? series.data : (xData || rawData), processPoint, doneProcessing);
  211. }
  212. }
  213. });
  214. /*
  215. * We need to handle heatmaps separatly, since we can't perform the
  216. * size/color calculations in the shader easily.
  217. *
  218. * This likely needs future optimization.
  219. */
  220. ['heatmap', 'treemap'].forEach(function (t) {
  221. if (seriesTypes[t]) {
  222. wrap(seriesTypes[t].prototype, 'drawPoints', pointDrawHandler);
  223. }
  224. });
  225. /* eslint-disable no-invalid-this */
  226. if (seriesTypes.bubble) {
  227. // By default, the bubble series does not use the KD-tree, so force it
  228. // to.
  229. delete seriesTypes.bubble.prototype.buildKDTree;
  230. // seriesTypes.bubble.prototype.directTouch = false;
  231. // Needed for markers to work correctly
  232. wrap(seriesTypes.bubble.prototype, 'markerAttribs', function (proceed) {
  233. if (this.isSeriesBoosting) {
  234. return false;
  235. }
  236. return proceed.apply(this, [].slice.call(arguments, 1));
  237. });
  238. }
  239. seriesTypes.scatter.prototype.fill = true;
  240. extend(seriesTypes.area.prototype, {
  241. fill: true,
  242. fillOpacity: true,
  243. sampling: true
  244. });
  245. extend(seriesTypes.column.prototype, {
  246. fill: true,
  247. sampling: true
  248. });
  249. Chart.prototype.propsRequireUpdateSeries.push('boost');
  250. // Take care of the canvas blitting
  251. Chart.prototype.callbacks.push(function (chart) {
  252. /**
  253. * Convert chart-level canvas to image.
  254. * @private
  255. */
  256. function canvasToSVG() {
  257. if (chart.ogl && chart.isChartSeriesBoosting()) {
  258. chart.ogl.render(chart);
  259. }
  260. }
  261. /**
  262. * Clear chart-level canvas.
  263. * @private
  264. */
  265. function preRender() {
  266. // Reset force state
  267. chart.boostForceChartBoost = void 0;
  268. chart.boostForceChartBoost = shouldForceChartSeriesBoosting(chart);
  269. chart.isBoosting = false;
  270. if (!chart.isChartSeriesBoosting() && chart.didBoost) {
  271. chart.didBoost = false;
  272. }
  273. // Clear the canvas
  274. if (chart.boostClear) {
  275. chart.boostClear();
  276. }
  277. if (chart.canvas && chart.ogl && chart.isChartSeriesBoosting()) {
  278. chart.didBoost = true;
  279. // Allocate
  280. chart.ogl.allocateBuffer(chart);
  281. }
  282. // see #6518 + #6739
  283. if (chart.markerGroup &&
  284. chart.xAxis &&
  285. chart.xAxis.length > 0 &&
  286. chart.yAxis &&
  287. chart.yAxis.length > 0) {
  288. chart.markerGroup.translate(chart.xAxis[0].pos, chart.yAxis[0].pos);
  289. }
  290. }
  291. addEvent(chart, 'predraw', preRender);
  292. addEvent(chart, 'render', canvasToSVG);
  293. // addEvent(chart, 'zoom', function () {
  294. // chart.boostForceChartBoost =
  295. // shouldForceChartSeriesBoosting(chart);
  296. // });
  297. var prevX = -1;
  298. var prevY = -1;
  299. addEvent(chart.pointer, 'afterGetHoverData', function () {
  300. var series = chart.hoverSeries;
  301. if (chart.markerGroup && series) {
  302. var xAxis = chart.inverted ? series.yAxis : series.xAxis;
  303. var yAxis = chart.inverted ? series.xAxis : series.yAxis;
  304. if ((xAxis && xAxis.pos !== prevX) ||
  305. (yAxis && yAxis.pos !== prevY)) {
  306. // #10464: Keep the marker group position in sync with the
  307. // position of the hovered series axes since there is only
  308. // one shared marker group when boosting.
  309. chart.markerGroup.translate(xAxis.pos, yAxis.pos);
  310. prevX = xAxis.pos;
  311. prevY = yAxis.pos;
  312. }
  313. }
  314. });
  315. });
  316. /* eslint-enable no-invalid-this */
  317. }
  318. export default init;