UNPKG

highcharts

Version:
263 lines (262 loc) 8.25 kB
/* * * * (c) 2019-2025 Highsoft AS * * Boost module: stripped-down renderer for higher performance * * License: highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ 'use strict'; import BoostableMap from './BoostableMap.js'; import H from '../../Core/Globals.js'; const { composed } = H; import U from '../../Core/Utilities.js'; const { addEvent, pick, pushUnique } = U; /* * * * Functions * * */ /** * @private */ function compose(ChartClass, wglMode) { if (wglMode && pushUnique(composed, 'Boost.Chart')) { ChartClass.prototype.callbacks.push(onChartCallback); } return ChartClass; } /** * Get the clip rectangle for a target, either a series or the chart. * For the chart, we need to consider the maximum extent of its Y axes, * in case of Highcharts Stock panes and navigator. * * @private * @function Highcharts.Chart#getBoostClipRect */ function getBoostClipRect(chart, target) { const navigator = chart.navigator; let clipBox = { x: chart.plotLeft, y: chart.plotTop, width: chart.plotWidth, height: chart.plotHeight }; if (navigator && chart.inverted) { // #17820, #20936 clipBox.width += navigator.top + navigator.height; if (!navigator.opposite) { clipBox.x = navigator.left; } } else if (navigator && !chart.inverted) { clipBox.height = navigator.top + navigator.height - chart.plotTop; } // Clipping of individual series (#11906, #19039). if (target.is) { const { xAxis, yAxis } = target; clipBox = chart.getClipBox(target); if (chart.inverted) { const lateral = clipBox.width; clipBox.width = clipBox.height; clipBox.height = lateral; clipBox.x = yAxis.pos; clipBox.y = xAxis.pos; } else { clipBox.x = xAxis.pos; clipBox.y = yAxis.pos; } } if (target === chart) { const verticalAxes = chart.inverted ? chart.xAxis : chart.yAxis; // #14444 if (verticalAxes.length <= 1) { clipBox.y = Math.min(verticalAxes[0].pos, clipBox.y); clipBox.height = (verticalAxes[0].pos - chart.plotTop + verticalAxes[0].len); } } return clipBox; } /** * Returns true if the chart is in series boost mode. * @private * @param {Highcharts.Chart} chart * Chart to check. * @return {boolean} * `true` if the chart is in series boost mode. */ function isChartSeriesBoosting(chart) { const allSeries = chart.series, boost = chart.boost = chart.boost || {}, boostOptions = chart.options.boost || {}, threshold = pick(boostOptions.seriesThreshold, 50); if (allSeries.length >= threshold) { return true; } if (allSeries.length === 1) { return false; } let allowBoostForce = boostOptions.allowForce; if (typeof allowBoostForce === 'undefined') { allowBoostForce = true; for (const axis of chart.xAxis) { if (pick(axis.min, -Infinity) > pick(axis.dataMin, -Infinity) || pick(axis.max, Infinity) < pick(axis.dataMax, Infinity)) { allowBoostForce = false; break; } } } if (typeof boost.forceChartBoost !== 'undefined') { if (allowBoostForce) { return boost.forceChartBoost; } boost.forceChartBoost = void 0; } // If there are more than five series currently boosting, // we should boost the whole chart to avoid running out of webgl contexts. let canBoostCount = 0, needBoostCount = 0, seriesOptions; for (const series of allSeries) { seriesOptions = series.options; // Don't count series with boostThreshold set to 0 // See #8950 // Also don't count if the series is hidden. // See #9046 if (seriesOptions.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.getColumn('x', true), seriesOptions.data, /// series.xData, series.points) >= (seriesOptions.boostThreshold || Number.MAX_VALUE)) { ++needBoostCount; } } boost.forceChartBoost = allowBoostForce && (( // Even when the series that need a boost are less than or equal // to 5, force a chart boost when all series are to be boosted. // See #18815 canBoostCount === allSeries.length && needBoostCount === canBoostCount) || needBoostCount > 5); return boost.forceChartBoost; } /** * Take care of the canvas blitting * @private */ function onChartCallback(chart) { /** * Convert chart-level canvas to image. * @private */ function canvasToSVG() { if (chart.boost && chart.boost.wgl && isChartSeriesBoosting(chart)) { chart.boost.wgl.render(chart); } } /** * Clear chart-level canvas. * @private */ function preRender() { // Reset force state chart.boost = chart.boost || {}; chart.boost.forceChartBoost = void 0; chart.boosted = false; // Clear the canvas if (!chart.axes.some((axis) => axis.isPanning)) { chart.boost.clear?.(); } if (chart.boost.canvas && chart.boost.wgl && isChartSeriesBoosting(chart)) { // Allocate chart.boost.wgl.allocateBuffer(chart); } // See #6518 + #6739 if (chart.boost.markerGroup && chart.xAxis && chart.xAxis.length > 0 && chart.yAxis && chart.yAxis.length > 0) { chart.boost.markerGroup.translate(chart.xAxis[0].pos, chart.yAxis[0].pos); } } addEvent(chart, 'predraw', preRender); // Use the load event rather than redraw, otherwise user load events will // fire too early (#18755) addEvent(chart, 'load', canvasToSVG, { order: -1 }); addEvent(chart, 'redraw', canvasToSVG); let prevX = -1; let prevY = -1; addEvent(chart.pointer, 'afterGetHoverData', (e) => { const series = e.hoverPoint?.series; chart.boost = chart.boost || {}; if (chart.boost.markerGroup && series) { const xAxis = chart.inverted ? series.yAxis : series.xAxis; const yAxis = chart.inverted ? series.xAxis : series.yAxis; if ((xAxis && xAxis.pos !== prevX) || (yAxis && yAxis.pos !== prevY)) { // #21176: If the axis is changed, hide teh halo without // animation to prevent flickering of halos sharing the // same marker group chart.series.forEach((s) => { s.halo?.hide(); }); // #10464: Keep the marker group position in sync with the // position of the hovered series axes since there is only // one shared marker group when boosting. chart.boost.markerGroup.translate(xAxis.pos, yAxis.pos); prevX = xAxis.pos; prevY = yAxis.pos; } } }); } /** * Tolerant max() function. * * @private * @param {...Array<Array<unknown>>} args * Max arguments * @return {number} * Max value */ function patientMax(...args) { let r = -Number.MAX_VALUE; args.forEach((t) => { if (typeof t !== 'undefined' && t !== null && typeof t.length !== 'undefined') { if (t.length > 0) { r = t.length; return true; } } }); return r; } /* * * * Default Export * * */ const BoostChart = { compose, getBoostClipRect, isChartSeriesBoosting }; export default BoostChart;