293 lines
12 KiB
JavaScript
293 lines
12 KiB
JavaScript
/* *
|
|
*
|
|
* 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 U from '../../parts/Utilities.js';
|
|
var addEvent = U.addEvent, extend = U.extend, fireEvent = U.fireEvent, wrap = U.wrap;
|
|
import '../../parts/Series.js';
|
|
import butils from './boost-utils.js';
|
|
import createAndAttachRenderer from './boost-attach.js';
|
|
var Series = H.Series, seriesTypes = H.seriesTypes, noop = function () { }, eachAsync = butils.eachAsync, pointDrawHandler = butils.pointDrawHandler, allocateIfNotSeriesBoosting = butils.allocateIfNotSeriesBoosting, renderIfNotSeriesBoosting = butils.renderIfNotSeriesBoosting, shouldForceChartSeriesBoosting = butils.shouldForceChartSeriesBoosting, index;
|
|
/* eslint-disable valid-jsdoc */
|
|
/**
|
|
* Initialize the boot module.
|
|
*
|
|
* @private
|
|
* @return {void}
|
|
*/
|
|
function init() {
|
|
extend(Series.prototype, {
|
|
/**
|
|
* @private
|
|
* @function Highcharts.Series#renderCanvas
|
|
*/
|
|
renderCanvas: function () {
|
|
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 &&
|
|
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 ||
|
|
this.options.xData ||
|
|
this.processedXData ||
|
|
false), addKDPoint = function (clientX, plotY, i) {
|
|
// We need to do ceil on the clientX to make things
|
|
// snap to pixel values. The renderer will frequently
|
|
// draw stuff on "sub-pixels".
|
|
clientX = Math.ceil(clientX);
|
|
// Shaves off about 60ms compared to repeated concatenation
|
|
index = compareX ? clientX : clientX + ',' + plotY;
|
|
// The k-d tree requires series points.
|
|
// Reduce the amount of points, since the time to build the
|
|
// tree increases exponentially.
|
|
if (enableMouseTracking && !pointTaken[index]) {
|
|
pointTaken[index] = true;
|
|
if (chart.inverted) {
|
|
clientX = xAxis.len - clientX;
|
|
plotY = yAxis.len - plotY;
|
|
}
|
|
points.push({
|
|
x: xDataFull ? xDataFull[cropStart + i] : false,
|
|
clientX: clientX,
|
|
plotX: clientX,
|
|
plotY: plotY,
|
|
i: cropStart + i
|
|
});
|
|
}
|
|
};
|
|
// Get or create the renderer
|
|
renderer = createAndAttachRenderer(chart, series);
|
|
chart.isBoosting = true;
|
|
boostOptions = renderer.settings;
|
|
if (!this.visible) {
|
|
return;
|
|
}
|
|
// If we are zooming out from SVG mode, destroy the graphics
|
|
if (this.points || this.graph) {
|
|
this.destroyGraphics();
|
|
}
|
|
// If we're rendering per. series we should create the marker groups
|
|
// as usual.
|
|
if (!chart.isChartSeriesBoosting()) {
|
|
// If all series were boosting, but are not anymore
|
|
// restore private markerGroup
|
|
if (this.markerGroup === chart.markerGroup) {
|
|
this.markerGroup = void 0;
|
|
}
|
|
this.markerGroup = series.plotGroup('markerGroup', 'markers', true, 1, chart.seriesGroup);
|
|
}
|
|
else {
|
|
// If series has a private markeGroup, remove that
|
|
// and use common markerGroup
|
|
if (this.markerGroup &&
|
|
this.markerGroup !== chart.markerGroup) {
|
|
this.markerGroup.destroy();
|
|
}
|
|
// Use a single group for the markers
|
|
this.markerGroup = chart.markerGroup;
|
|
// When switching from chart boosting mode, destroy redundant
|
|
// series boosting targets
|
|
if (this.renderTarget) {
|
|
this.renderTarget = this.renderTarget.destroy();
|
|
}
|
|
}
|
|
points = this.points = [];
|
|
// Do not start building while drawing
|
|
series.buildKDTree = noop;
|
|
if (renderer) {
|
|
allocateIfNotSeriesBoosting(renderer, this);
|
|
renderer.pushSeries(series);
|
|
// Perform the actual renderer if we're on series level
|
|
renderIfNotSeriesBoosting(renderer, this, chart);
|
|
}
|
|
/**
|
|
* This builds the KD-tree
|
|
* @private
|
|
*/
|
|
function processPoint(d, i) {
|
|
var x, y, clientX, plotY, isNull, low = false, chartDestroyed = typeof chart.index === 'undefined', isYInside = true;
|
|
if (!chartDestroyed) {
|
|
if (useRaw) {
|
|
x = d[0];
|
|
y = d[1];
|
|
}
|
|
else {
|
|
x = d;
|
|
y = yData[i];
|
|
}
|
|
// Resolve low and high for range series
|
|
if (isRange) {
|
|
if (useRaw) {
|
|
y = d.slice(1, 3);
|
|
}
|
|
low = y[0];
|
|
y = y[1];
|
|
}
|
|
else if (isStacked) {
|
|
x = d.x;
|
|
y = d.stackY;
|
|
low = y - d.y;
|
|
}
|
|
isNull = y === null;
|
|
// Optimize for scatter zooming
|
|
if (!requireSorting) {
|
|
isYInside = y >= yMin && y <= yMax;
|
|
}
|
|
if (!isNull && x >= xMin && x <= xMax && isYInside) {
|
|
clientX = xAxis.toPixels(x, true);
|
|
if (sampling) {
|
|
if (typeof minI === 'undefined' ||
|
|
clientX === lastClientX) {
|
|
if (!isRange) {
|
|
low = y;
|
|
}
|
|
if (typeof maxI === 'undefined' ||
|
|
y > maxVal) {
|
|
maxVal = y;
|
|
maxI = i;
|
|
}
|
|
if (typeof minI === 'undefined' ||
|
|
low < minVal) {
|
|
minVal = low;
|
|
minI = i;
|
|
}
|
|
}
|
|
// Add points and reset
|
|
if (clientX !== lastClientX) {
|
|
// maxI is number too:
|
|
if (typeof minI !== 'undefined') {
|
|
plotY =
|
|
yAxis.toPixels(maxVal, true);
|
|
yBottom =
|
|
yAxis.toPixels(minVal, true);
|
|
addKDPoint(clientX, plotY, maxI);
|
|
if (yBottom !== plotY) {
|
|
addKDPoint(clientX, yBottom, minI);
|
|
}
|
|
}
|
|
minI = maxI = void 0;
|
|
lastClientX = clientX;
|
|
}
|
|
}
|
|
else {
|
|
plotY = Math.ceil(yAxis.toPixels(y, true));
|
|
addKDPoint(clientX, plotY, i);
|
|
}
|
|
}
|
|
}
|
|
return !chartDestroyed;
|
|
}
|
|
/**
|
|
* @private
|
|
*/
|
|
function doneProcessing() {
|
|
fireEvent(series, 'renderedCanvas');
|
|
// Go back to prototype, ready to build
|
|
delete series.buildKDTree;
|
|
series.buildKDTree();
|
|
if (boostOptions.debug.timeKDTree) {
|
|
console.timeEnd('kd tree building'); // eslint-disable-line no-console
|
|
}
|
|
}
|
|
// Loop over the points to build the k-d tree - skip this if
|
|
// exporting
|
|
if (!chart.renderer.forExport) {
|
|
if (boostOptions.debug.timeKDTree) {
|
|
console.time('kd tree building'); // eslint-disable-line no-console
|
|
}
|
|
eachAsync(isStacked ? series.data : (xData || rawData), processPoint, doneProcessing);
|
|
}
|
|
}
|
|
});
|
|
/*
|
|
* We need to handle heatmaps separatly, since we can't perform the
|
|
* size/color calculations in the shader easily.
|
|
*
|
|
* This likely needs future optimization.
|
|
*/
|
|
['heatmap', 'treemap'].forEach(function (t) {
|
|
if (seriesTypes[t]) {
|
|
wrap(seriesTypes[t].prototype, 'drawPoints', pointDrawHandler);
|
|
}
|
|
});
|
|
/* eslint-disable no-invalid-this */
|
|
if (seriesTypes.bubble) {
|
|
// By default, the bubble series does not use the KD-tree, so force it
|
|
// to.
|
|
delete seriesTypes.bubble.prototype.buildKDTree;
|
|
// seriesTypes.bubble.prototype.directTouch = false;
|
|
// Needed for markers to work correctly
|
|
wrap(seriesTypes.bubble.prototype, 'markerAttribs', function (proceed) {
|
|
if (this.isSeriesBoosting) {
|
|
return false;
|
|
}
|
|
return proceed.apply(this, [].slice.call(arguments, 1));
|
|
});
|
|
}
|
|
seriesTypes.scatter.prototype.fill = true;
|
|
extend(seriesTypes.area.prototype, {
|
|
fill: true,
|
|
fillOpacity: true,
|
|
sampling: true
|
|
});
|
|
extend(seriesTypes.column.prototype, {
|
|
fill: true,
|
|
sampling: true
|
|
});
|
|
// Take care of the canvas blitting
|
|
H.Chart.prototype.callbacks.push(function (chart) {
|
|
/**
|
|
* Convert chart-level canvas to image.
|
|
* @private
|
|
*/
|
|
function canvasToSVG() {
|
|
if (chart.ogl && chart.isChartSeriesBoosting()) {
|
|
chart.ogl.render(chart);
|
|
}
|
|
}
|
|
/**
|
|
* Clear chart-level canvas.
|
|
* @private
|
|
*/
|
|
function preRender() {
|
|
// Reset force state
|
|
chart.boostForceChartBoost = void 0;
|
|
chart.boostForceChartBoost = shouldForceChartSeriesBoosting(chart);
|
|
chart.isBoosting = false;
|
|
if (!chart.isChartSeriesBoosting() && chart.didBoost) {
|
|
chart.didBoost = false;
|
|
}
|
|
// Clear the canvas
|
|
if (chart.boostClear) {
|
|
chart.boostClear();
|
|
}
|
|
if (chart.canvas && chart.ogl && chart.isChartSeriesBoosting()) {
|
|
chart.didBoost = true;
|
|
// Allocate
|
|
chart.ogl.allocateBuffer(chart);
|
|
}
|
|
// see #6518 + #6739
|
|
if (chart.markerGroup &&
|
|
chart.xAxis &&
|
|
chart.xAxis.length > 0 &&
|
|
chart.yAxis &&
|
|
chart.yAxis.length > 0) {
|
|
chart.markerGroup.translate(chart.xAxis[0].pos, chart.yAxis[0].pos);
|
|
}
|
|
}
|
|
addEvent(chart, 'predraw', preRender);
|
|
addEvent(chart, 'render', canvasToSVG);
|
|
// addEvent(chart, 'zoom', function () {
|
|
// chart.boostForceChartBoost =
|
|
// shouldForceChartSeriesBoosting(chart);
|
|
// });
|
|
});
|
|
/* eslint-enable no-invalid-this */
|
|
}
|
|
export default init;
|