/* * * * Copyright (c) 2019-2020 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 H from '../../parts/Globals.js'; import Point from '../../parts/Point.js'; import U from '../../parts/Utilities.js'; var addEvent = U.addEvent, error = U.error, isArray = U.isArray, isNumber = U.isNumber, pick = U.pick, wrap = U.wrap; import '../../parts/Series.js'; import '../../parts/Options.js'; import '../../parts/Interaction.js'; import butils from './boost-utils.js'; import boostable from './boostables.js'; import boostableMap from './boostable-map.js'; var boostEnabled = butils.boostEnabled, shouldForceChartSeriesBoosting = butils.shouldForceChartSeriesBoosting, Chart = H.Chart, Series = H.Series, seriesTypes = H.seriesTypes, plotOptions = H.getOptions().plotOptions; /** * Returns true if the chart is in series boost mode. * * @function Highcharts.Chart#isChartSeriesBoosting * * @param {Highcharts.Chart} chart * the chart to check * * @return {boolean} * true if the chart is in series boost mode */ Chart.prototype.isChartSeriesBoosting = function () { var isSeriesBoosting, threshold = pick(this.options.boost && this.options.boost.seriesThreshold, 50); isSeriesBoosting = threshold <= this.series.length || shouldForceChartSeriesBoosting(this); return isSeriesBoosting; }; /* eslint-disable valid-jsdoc */ /** * 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 * Highstock panes and navigator. * * @private * @function Highcharts.Chart#getBoostClipRect * * @param {Highcharts.Chart} target * * @return {Highcharts.BBoxObject} */ Chart.prototype.getBoostClipRect = function (target) { var clipBox = { x: this.plotLeft, y: this.plotTop, width: this.plotWidth, height: this.plotHeight }; if (target === this) { this.yAxis.forEach(function (yAxis) { clipBox.y = Math.min(yAxis.pos, clipBox.y); clipBox.height = Math.max(yAxis.pos - this.plotTop + yAxis.len, clipBox.height); }, this); } return clipBox; }; /** * Return a full Point object based on the index. * The boost module uses stripped point objects for performance reasons. * * @function Highcharts.Series#getPoint * * @param {object|Highcharts.Point} boostPoint * A stripped-down point object * * @return {Highcharts.Point} * A Point object as per https://api.highcharts.com/highcharts#Point */ Series.prototype.getPoint = function (boostPoint) { var point = boostPoint, xData = (this.xData || this.options.xData || this.processedXData || false); if (boostPoint && !(boostPoint instanceof this.pointClass)) { point = (new this.pointClass()).init(// eslint-disable-line new-cap this, this.options.data[boostPoint.i], xData ? xData[boostPoint.i] : void 0); point.category = pick(this.xAxis.categories ? this.xAxis.categories[point.x] : point.x, // @todo simplify point.x); point.dist = boostPoint.dist; point.distX = boostPoint.distX; point.plotX = boostPoint.plotX; point.plotY = boostPoint.plotY; point.index = boostPoint.i; point.isInside = this.isPointInside(boostPoint); } return point; }; /* eslint-disable no-invalid-this */ // Return a point instance from the k-d-tree wrap(Series.prototype, 'searchPoint', function (proceed) { return this.getPoint(proceed.apply(this, [].slice.call(arguments, 1))); }); // For inverted series, we need to swap X-Y values before running base methods wrap(Point.prototype, 'haloPath', function (proceed) { var halo, point = this, series = point.series, chart = series.chart, plotX = point.plotX, plotY = point.plotY, inverted = chart.inverted; if (series.isSeriesBoosting && inverted) { point.plotX = series.yAxis.len - plotY; point.plotY = series.xAxis.len - plotX; } halo = proceed.apply(this, Array.prototype.slice.call(arguments, 1)); if (series.isSeriesBoosting && inverted) { point.plotX = plotX; point.plotY = plotY; } return halo; }); wrap(Series.prototype, 'markerAttribs', function (proceed, point) { var attribs, series = this, chart = series.chart, plotX = point.plotX, plotY = point.plotY, inverted = chart.inverted; if (series.isSeriesBoosting && inverted) { point.plotX = series.yAxis.len - plotY; point.plotY = series.xAxis.len - plotX; } attribs = proceed.apply(this, Array.prototype.slice.call(arguments, 1)); if (series.isSeriesBoosting && inverted) { point.plotX = plotX; point.plotY = plotY; } return attribs; }); /* * Extend series.destroy to also remove the fake k-d-tree points (#5137). * Normally this is handled by Series.destroy that calls Point.destroy, * but the fake search points are not registered like that. */ addEvent(Series, 'destroy', function () { var series = this, chart = series.chart; if (chart.markerGroup === series.markerGroup) { series.markerGroup = null; } if (chart.hoverPoints) { chart.hoverPoints = chart.hoverPoints.filter(function (point) { return point.series === series; }); } if (chart.hoverPoint && chart.hoverPoint.series === series) { chart.hoverPoint = null; } }); /* * Do not compute extremes when min and max are set. * If we use this in the core, we can add the hook * to hasExtremes to the methods directly. */ wrap(Series.prototype, 'getExtremes', function (proceed) { if (!this.isSeriesBoosting || (!this.hasExtremes || !this.hasExtremes())) { return proceed.apply(this, Array.prototype.slice.call(arguments, 1)); } return {}; }); /* * Override a bunch of methods the same way. If the number of points is * below the threshold, run the original method. If not, check for a * canvas version or do nothing. * * Note that we're not overriding any of these for heatmaps. */ [ 'translate', 'generatePoints', 'drawTracker', 'drawPoints', 'render' ].forEach(function (method) { /** * @private */ function branch(proceed) { var letItPass = this.options.stacking && (method === 'translate' || method === 'generatePoints'); if (!this.isSeriesBoosting || letItPass || !boostEnabled(this.chart) || this.type === 'heatmap' || this.type === 'treemap' || !boostableMap[this.type] || this.options.boostThreshold === 0) { proceed.call(this); // If a canvas version of the method exists, like renderCanvas(), run } else if (this[method + 'Canvas']) { this[method + 'Canvas'](); } } wrap(Series.prototype, method, branch); // A special case for some types - their translate method is already wrapped if (method === 'translate') { [ 'column', 'bar', 'arearange', 'columnrange', 'heatmap', 'treemap' ].forEach(function (type) { if (seriesTypes[type]) { wrap(seriesTypes[type].prototype, method, branch); } }); } }); // If the series is a heatmap or treemap, or if the series is not boosting // do the default behaviour. Otherwise, process if the series has no extremes. wrap(Series.prototype, 'processData', function (proceed) { var series = this, dataToMeasure = this.options.data, firstPoint; /** * Used twice in this function, first on this.options.data, the second * time it runs the check again after processedXData is built. * @private * @todo Check what happens with data grouping */ function getSeriesBoosting(data) { return series.chart.isChartSeriesBoosting() || ((data ? data.length : 0) >= (series.options.boostThreshold || Number.MAX_VALUE)); } if (boostEnabled(this.chart) && boostableMap[this.type]) { // If there are no extremes given in the options, we also need to // process the data to read the data extremes. If this is a heatmap, do // default behaviour. if (!getSeriesBoosting(dataToMeasure) || // First pass with options.data this.type === 'heatmap' || this.type === 'treemap' || this.options.stacking || // processedYData for the stack (#7481) !this.hasExtremes || !this.hasExtremes(true)) { proceed.apply(this, Array.prototype.slice.call(arguments, 1)); dataToMeasure = this.processedXData; } // Set the isBoosting flag, second pass with processedXData to see if we // have zoomed. this.isSeriesBoosting = getSeriesBoosting(dataToMeasure); // Enter or exit boost mode if (this.isSeriesBoosting) { // Force turbo-mode: firstPoint = this.getFirstValidPoint(this.options.data); if (!isNumber(firstPoint) && !isArray(firstPoint)) { error(12, false, this.chart); } this.enterBoost(); } else if (this.exitBoost) { this.exitBoost(); } // The series type is not boostable } else { proceed.apply(this, Array.prototype.slice.call(arguments, 1)); } }); addEvent(Series, 'hide', function () { if (this.canvas && this.renderTarget) { if (this.ogl) { this.ogl.clear(); } this.boostClear(); } }); /** * Enter boost mode and apply boost-specific properties. * * @function Highcharts.Series#enterBoost */ Series.prototype.enterBoost = function () { this.alteredByBoost = []; // Save the original values, including whether it was an own property or // inherited from the prototype. ['allowDG', 'directTouch', 'stickyTracking'].forEach(function (prop) { this.alteredByBoost.push({ prop: prop, val: this[prop], own: Object.hasOwnProperty.call(this, prop) }); }, this); this.allowDG = false; this.directTouch = false; this.stickyTracking = true; // Hide series label if any if (this.labelBySeries) { this.labelBySeries = this.labelBySeries.destroy(); } }; /** * Exit from boost mode and restore non-boost properties. * * @function Highcharts.Series#exitBoost */ Series.prototype.exitBoost = function () { // Reset instance properties and/or delete instance properties and go back // to prototype (this.alteredByBoost || []).forEach(function (setting) { if (setting.own) { this[setting.prop] = setting.val; } else { // Revert to prototype delete this[setting.prop]; } }, this); // Clear previous run if (this.boostClear) { this.boostClear(); } }; /** * @private * @function Highcharts.Series#hasExtremes * * @param {boolean} checkX * * @return {boolean} */ Series.prototype.hasExtremes = function (checkX) { 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; return data.length > (options.boostThreshold || Number.MAX_VALUE) && // Defined yAxis extremes isNumber(yAxis.min) && isNumber(yAxis.max) && // Defined (and required) xAxis extremes (!checkX || (isNumber(xAxis.min) && isNumber(xAxis.max))) && // Defined (e.g. heatmap) colorAxis extremes (!colorAxis || (isNumber(colorAxis.min) && isNumber(colorAxis.max))); }; /** * If implemented in the core, parts of this can probably be * shared with other similar methods in Highcharts. * * @function Highcharts.Series#destroyGraphics */ Series.prototype.destroyGraphics = function () { var series = this, points = this.points, point, i; if (points) { for (i = 0; i < points.length; i = i + 1) { point = points[i]; if (point && point.destroyElements) { point.destroyElements(); // #7557 } } } ['graph', 'area', 'tracker'].forEach(function (prop) { if (series[prop]) { series[prop] = series[prop].destroy(); } }); }; // Set default options boostable.forEach(function (type) { if (plotOptions[type]) { plotOptions[type].boostThreshold = 5000; plotOptions[type].boostData = []; seriesTypes[type].prototype.fillOpacity = true; } });