Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve heatmap rendering performance when zsmooth is false #6574

Merged
merged 10 commits into from
May 12, 2023
1 change: 1 addition & 0 deletions draftlogs/6574_change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Improve heatmap rendering performance when `zsmooth` is set to false [[#6574](https://github.com/plotly/plotly.js/pull/6574)], with thanks to @lvlte for the contribution!
22 changes: 22 additions & 0 deletions src/constants/pixelated_image.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict';

// Pixelated image rendering
// The actual CSS declaration is prepended with fallbacks for older browsers.
// NB. IE's `-ms-interpolation-mode` works only with <img> not with SVG <image>
// https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering
// https://caniuse.com/?search=image-rendering
// http://phrogz.net/tmp/canvas_image_zoom.html

exports.CSS_DECLARATIONS = [
['image-rendering', 'optimizeSpeed'],
['image-rendering', '-moz-crisp-edges'],
['image-rendering', '-o-crisp-edges'],
['image-rendering', '-webkit-optimize-contrast'],
['image-rendering', 'optimize-contrast'],
['image-rendering', 'crisp-edges'],
['image-rendering', 'pixelated']
];

exports.STYLE = exports.CSS_DECLARATIONS.map(function(d) {
return d.join(': ') + '; ';
}).join('');
42 changes: 42 additions & 0 deletions src/lib/supports_pixelated_image.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use strict';

var constants = require('../constants/pixelated_image');
var Drawing = require('../components/drawing');
var Lib = require('../lib');

var _supportsPixelated = null;

/**
* Check browser support for pixelated image rendering
*
* @return {boolean}
*/
function supportsPixelatedImage() {
if(_supportsPixelated !== null) { // only run the feature detection once
return _supportsPixelated;
}
if(Lib.isIE()) {
_supportsPixelated = false;
} else {
var declarations = Array.from(constants.CSS_DECLARATIONS).reverse();
var supports = window.CSS && window.CSS.supports || window.supportsCSS;
if(typeof supports === 'function') {
_supportsPixelated = declarations.some(function(d) {
return supports.apply(null, d);
});
} else {
var image3 = Drawing.tester.append('image');
var cStyles = window.getComputedStyle(image3.node());
image3.attr('style', constants.STYLE);
_supportsPixelated = declarations.some(function(d) {
var value = d[1];
return cStyles.imageRendering === value ||
cStyles.imageRendering === value.toLowerCase();
});
image3.remove();
}
}
return _supportsPixelated;
}

module.exports = supportsPixelatedImage;
45 changes: 22 additions & 23 deletions src/traces/heatmap/calc.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,32 +90,31 @@ module.exports = function calc(gd, trace) {
Lib.warn('cannot use zsmooth: "fast": ' + msg);
}

// check whether we really can smooth (ie all boxes are about the same size)
if(zsmooth === 'fast') {
if(xa.type === 'log' || ya.type === 'log') {
noZsmooth('log axis found');
} else if(!isHist) {
if(x.length) {
var avgdx = (x[x.length - 1] - x[0]) / (x.length - 1);
var maxErrX = Math.abs(avgdx / 100);
for(i = 0; i < x.length - 1; i++) {
if(Math.abs(x[i + 1] - x[i] - avgdx) > maxErrX) {
noZsmooth('x scale is not linear');
break;
}
}
}
if(y.length && zsmooth === 'fast') {
var avgdy = (y[y.length - 1] - y[0]) / (y.length - 1);
var maxErrY = Math.abs(avgdy / 100);
for(i = 0; i < y.length - 1; i++) {
if(Math.abs(y[i + 1] - y[i] - avgdy) > maxErrY) {
noZsmooth('y scale is not linear');
break;
}
function scaleIsLinear(s) {
if(s.length > 1) {
var avgdx = (s[s.length - 1] - s[0]) / (s.length - 1);
var maxErrX = Math.abs(avgdx / 100);
for(i = 0; i < s.length - 1; i++) {
if(Math.abs(s[i + 1] - s[i] - avgdx) > maxErrX) {
return false;
}
}
}
return true;
}

// Check whether all brick are uniform
trace._islinear = false;
if(xa.type === 'log' || ya.type === 'log') {
if(zsmooth === 'fast') {
noZsmooth('log axis found');
}
} else if(!scaleIsLinear(x)) {
if(zsmooth === 'fast') noZsmooth('x scale is not linear');
} else if(!scaleIsLinear(y)) {
if(zsmooth === 'fast') noZsmooth('y scale is not linear');
} else {
trace._islinear = true;
}

// create arrays of brick boundaries, to be used by autorange and heatmap.plot
Expand Down
34 changes: 25 additions & 9 deletions src/traces/heatmap/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ var makeColorScaleFuncFromTrace = require('../../components/colorscale').makeCol
var xmlnsNamespaces = require('../../constants/xmlns_namespaces');
var alignmentConstants = require('../../constants/alignment');
var LINE_SPACING = alignmentConstants.LINE_SPACING;
var supportsPixelatedImage = require('../../lib/supports_pixelated_image');
var PIXELATED_IMAGE_STYLE = require('../../constants/pixelated_image').STYLE;

var labelClass = 'heatmap-label';

Expand Down Expand Up @@ -109,11 +111,18 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
y = cd0.yfill;
}

var drawingMethod = 'default';
if(zsmooth) {
drawingMethod = zsmooth === 'best' ? 'smooth' : 'fast';
} else if(trace._islinear && xGap === 0 && yGap === 0 && supportsPixelatedImage()) {
drawingMethod = 'fast';
}

// make an image that goes at most half a screen off either side, to keep
// time reasonable when you zoom in. if zsmooth is true/fast, don't worry
// time reasonable when you zoom in. if drawingMethod is fast, don't worry
// about this, because zooming doesn't increase number of pixels
// if zsmooth is best, don't include anything off screen because it takes too long
if(zsmooth !== 'fast') {
if(drawingMethod !== 'fast') {
var extra = zsmooth === 'best' ? 0 : 0.5;
left = Math.max(-extra * xa._length, left);
right = Math.min((1 + extra) * xa._length, right);
Expand All @@ -127,7 +136,9 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
// setup image nodes

// if image is entirely off-screen, don't even draw it
var isOffScreen = (imageWidth <= 0 || imageHeight <= 0);
var isOffScreen = (
left >= xa._length || right <= 0 || top >= ya._length || bottom <= 0
);

if(isOffScreen) {
var noImage = plotGroup.selectAll('image').data([]);
Expand All @@ -140,7 +151,7 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
// generate image data

var canvasW, canvasH;
if(zsmooth === 'fast') {
if(drawingMethod === 'fast') {
canvasW = n;
canvasH = m;
} else {
Expand All @@ -158,7 +169,7 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
// map brick boundaries to image pixels
var xpx,
ypx;
if(zsmooth === 'fast') {
if(drawingMethod === 'fast') {
xpx = xrev ?
function(index) { return n - 1 - index; } :
Lib.identity;
Expand Down Expand Up @@ -235,7 +246,7 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
return setColor(z00 + xinterp.frac * dx + yinterp.frac * (dy + xinterp.frac * dxy));
}

if(zsmooth) { // best or fast, works fastest with imageData
if(drawingMethod !== 'default') { // works fastest with imageData
var pxIndex = 0;
var pixels;

Expand All @@ -245,7 +256,7 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
pixels = new Array(canvasW * canvasH * 4);
}

if(zsmooth === 'best') {
if(drawingMethod === 'smooth') { // zsmooth="best"
var xForPx = xc || x;
var yForPx = yc || y;
var xPixArray = new Array(xForPx.length);
Expand Down Expand Up @@ -273,7 +284,7 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
putColor(pixels, pxIndex, c);
}
}
} else { // zsmooth = fast
} else { // drawingMethod = "fast" (zsmooth = "fast"|false)
for(j = 0; j < m; j++) {
row = z[j];
yb = ypx(j);
Expand All @@ -297,7 +308,8 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
}

context.putImageData(imageData, 0, 0);
} else { // zsmooth = false -> filling potentially large bricks works fastest with fillRect
} else { // rawingMethod = "default" (zsmooth = false)
// filling potentially large bricks works fastest with fillRect
// gaps do not need to be exact integers, but if they *are* we will get
// cleaner edges by rounding at least one edge
var xGapLeft = Math.floor(xGap / 2);
Expand Down Expand Up @@ -353,6 +365,10 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
'xlink:href': canvas.toDataURL('image/png')
});

if(drawingMethod === 'fast' && !zsmooth) {
image3.attr('style', PIXELATED_IMAGE_STYLE);
}

removeLabels(plotGroup);

var texttemplate = trace.texttemplate;
Expand Down
15 changes: 1 addition & 14 deletions src/traces/image/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,5 @@ module.exports = {
},
suffix: ['°', '%', '%', '']
}
},
// For pixelated image rendering
// http://phrogz.net/tmp/canvas_image_zoom.html
// https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering
pixelatedStyle: [
'image-rendering: optimizeSpeed',
'image-rendering: -moz-crisp-edges',
'image-rendering: -o-crisp-edges',
'image-rendering: -webkit-optimize-contrast',
'image-rendering: optimize-contrast',
'image-rendering: crisp-edges',
'image-rendering: pixelated',
''
].join('; ')
}
};
10 changes: 5 additions & 5 deletions src/traces/image/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@ var Lib = require('../../lib');
var strTranslate = Lib.strTranslate;
var xmlnsNamespaces = require('../../constants/xmlns_namespaces');
var constants = require('./constants');

var unsupportedBrowsers = Lib.isIOS() || Lib.isSafari() || Lib.isIE();
var supportsPixelatedImage = require('../../lib/supports_pixelated_image');
var PIXELATED_IMAGE_STYLE = require('../../constants/pixelated_image').STYLE;

module.exports = function plot(gd, plotinfo, cdimage, imageLayer) {
var xa = plotinfo.xaxis;
var ya = plotinfo.yaxis;

var supportsPixelatedImage = !(unsupportedBrowsers || gd._context._exportedPlot);
var supportsPixelated = !gd._context._exportedPlot && supportsPixelatedImage();

Lib.makeTraceGroups(imageLayer, cdimage, 'im').each(function(cd) {
var plotGroup = d3.select(this);
var cd0 = cd[0];
var trace = cd0.trace;
var realImage = (
((trace.zsmooth === 'fast') || (trace.zsmooth === false && supportsPixelatedImage)) &&
((trace.zsmooth === 'fast') || (trace.zsmooth === false && supportsPixelated)) &&
!trace._hasZ && trace._hasSource && xa.type === 'linear' && ya.type === 'linear'
);
trace._realImage = realImage;
Expand Down Expand Up @@ -131,7 +131,7 @@ module.exports = function plot(gd, plotinfo, cdimage, imageLayer) {

image3.exit().remove();

var style = (trace.zsmooth === false) ? constants.pixelatedStyle : '';
var style = (trace.zsmooth === false) ? PIXELATED_IMAGE_STYLE : '';

if(realImage) {
var xRange = Lib.simpleMap(xa.range, xa.r2l);
Expand Down
2 changes: 1 addition & 1 deletion tasks/test_syntax.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ function assertSrcContents() {
* - If you use conforms to these rules, you may update
* KNOWN_GET_COMPUTED_STYLE_CALLS to count the new use.
*/
var KNOWN_GET_COMPUTED_STYLE_CALLS = 6;
var KNOWN_GET_COMPUTED_STYLE_CALLS = 7;
if(getComputedStyleCnt !== KNOWN_GET_COMPUTED_STYLE_CALLS) {
logs.push('Expected ' + KNOWN_GET_COMPUTED_STYLE_CALLS +
' window.getComputedStyle calls, found ' + getComputedStyleCnt +
Expand Down
Binary file modified test/image/baselines/cmid-zmid.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/image/baselines/dendrogram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/image/baselines/heatmap_multicategory.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/image/baselines/histogram2d_bingroup-coloraxis.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/image/baselines/updatemenus.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading