/* * * * Copyright (c) 2019-2020 Highsoft AS * * Boost module: stripped-down renderer for higher performance * * License: highcharts.com/license * * This files contains generic utility functions used by the boost module. * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ 'use strict'; import H from '../../parts/Globals.js'; import '../../parts/Series.js'; import boostableMap from './boostable-map.js'; import createAndAttachRenderer from './boost-attach.js'; import U from '../../parts/Utilities.js'; var pick = U.pick; var win = H.win, doc = win.document; // This should be a const. var CHUNK_SIZE = 3000; /** * Tolerant max() function. * * @private * @function patientMax * * @param {...Array>} args * Max arguments * * @return {number} * Max value */ function patientMax() { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } var r = -Number.MAX_VALUE; args.forEach(function (t) { if (typeof t !== 'undefined' && t !== null && typeof t.length !== 'undefined') { // r = r < t.length ? t.length : r; if (t.length > 0) { r = t.length; return true; } } }); return r; } /** * Return true if ths boost.enabled option is true * * @private * @function boostEnabled * * @param {Highcharts.Chart} chart * The chart * * @return {boolean} * True, if boost is enabled. */ function boostEnabled(chart) { return pick((chart && chart.options && chart.options.boost && chart.options.boost.enabled), true); } /** * Returns true if we should force boosting the chart * @private * @function shouldForceChartSeriesBoosting * * @param {Highcharts.Chart} chart * The chart to check for forcing on * * @return {boolean} * True, if boosting should be forced. */ function shouldForceChartSeriesBoosting(chart) { // If there are more than five series currently boosting, // we should boost the whole chart to avoid running out of webgl contexts. var sboostCount = 0, canBoostCount = 0, allowBoostForce = pick(chart.options.boost && chart.options.boost.allowForce, true), series; if (typeof chart.boostForceChartBoost !== 'undefined') { return chart.boostForceChartBoost; } if (chart.series.length > 1) { for (var i = 0; i < chart.series.length; i++) { series = chart.series[i]; // Don't count series with boostThreshold set to 0 // See #8950 // Also don't count if the series is hidden. // See #9046 if (series.options.boostThreshold === 0 || series.visible === false) { continue; } // Don't count heatmap series as they are handled differently. // In the future we should make the heatmap/treemap path compatible // with forcing. See #9636. if (series.type === 'heatmap') { continue; } if (boostableMap[series.type]) { ++canBoostCount; } if (patientMax(series.processedXData, series.options.data, // series.xData, series.points) >= (series.options.boostThreshold || Number.MAX_VALUE)) { ++sboostCount; } } } chart.boostForceChartBoost = allowBoostForce && ((canBoostCount === chart.series.length && sboostCount > 0) || sboostCount > 5); return chart.boostForceChartBoost; } /* eslint-disable valid-jsdoc */ /** * Performs the actual render if the renderer is * attached to the series. * @private * @param renderer {OGLRenderer} - the renderer * @param series {Highcharts.Series} - the series */ function renderIfNotSeriesBoosting(renderer, series, chart) { if (renderer && series.renderTarget && series.canvas && !(chart || series.chart).isChartSeriesBoosting()) { renderer.render(chart || series.chart); } } /** * @private */ function allocateIfNotSeriesBoosting(renderer, series) { if (renderer && series.renderTarget && series.canvas && !series.chart.isChartSeriesBoosting()) { renderer.allocateBufferForSingleSeries(series); } } /** * An "async" foreach loop. Uses a setTimeout to keep the loop from blocking the * UI thread. * * @private * * @param arr {Array} - the array to loop through * @param fn {Function} - the callback to call for each item * @param finalFunc {Function} - the callback to call when done * @param chunkSize {Number} - the number of iterations per timeout * @param i {Number} - the current index * @param noTimeout {Boolean} - set to true to skip timeouts */ function eachAsync(arr, fn, finalFunc, chunkSize, i, noTimeout) { i = i || 0; chunkSize = chunkSize || CHUNK_SIZE; var threshold = i + chunkSize, proceed = true; while (proceed && i < threshold && i < arr.length) { proceed = fn(arr[i], i); ++i; } if (proceed) { if (i < arr.length) { if (noTimeout) { eachAsync(arr, fn, finalFunc, chunkSize, i, noTimeout); } else if (win.requestAnimationFrame) { // If available, do requestAnimationFrame - shaves off a few ms win.requestAnimationFrame(function () { eachAsync(arr, fn, finalFunc, chunkSize, i); }); } else { setTimeout(function () { eachAsync(arr, fn, finalFunc, chunkSize, i); }); } } else if (finalFunc) { finalFunc(); } } } /** * Returns true if the current browser supports webgl * * @private * @function hasWebGLSupport * * @return {boolean} */ function hasWebGLSupport() { var i = 0, canvas, contexts = ['webgl', 'experimental-webgl', 'moz-webgl', 'webkit-3d'], context = false; if (typeof win.WebGLRenderingContext !== 'undefined') { canvas = doc.createElement('canvas'); for (; i < contexts.length; i++) { try { context = canvas.getContext(contexts[i]); if (typeof context !== 'undefined' && context !== null) { return true; } } catch (e) { // silent error } } } return false; } /* eslint-disable no-invalid-this */ /** * Used for treemap|heatmap.drawPoints * * @private * @function pointDrawHandler * * @param {Function} proceed * * @return {*} */ function pointDrawHandler(proceed) { var enabled = true, renderer; if (this.chart.options && this.chart.options.boost) { enabled = typeof this.chart.options.boost.enabled === 'undefined' ? true : this.chart.options.boost.enabled; } if (!enabled || !this.isSeriesBoosting) { return proceed.call(this); } this.chart.isBoosting = true; // Make sure we have a valid OGL context renderer = createAndAttachRenderer(this.chart, this); if (renderer) { allocateIfNotSeriesBoosting(renderer, this); renderer.pushSeries(this); } renderIfNotSeriesBoosting(renderer, this); } /* eslint-enable no-invalid-this, valid-jsdoc */ var funs = { patientMax: patientMax, boostEnabled: boostEnabled, shouldForceChartSeriesBoosting: shouldForceChartSeriesBoosting, renderIfNotSeriesBoosting: renderIfNotSeriesBoosting, allocateIfNotSeriesBoosting: allocateIfNotSeriesBoosting, eachAsync: eachAsync, hasWebGLSupport: hasWebGLSupport, pointDrawHandler: pointDrawHandler }; // This needs to be fixed. H.hasWebGLSupport = hasWebGLSupport; export default funs;