Skip to content

Commit

Permalink
Merge pull request #6574 from lvlte/heatmap_rendering_perf
Browse files Browse the repository at this point in the history
Improve heatmap rendering performance when `zsmooth` is false
  • Loading branch information
archmoj authored May 12, 2023
2 parents a4ce2b7 + 4eaf62e commit 44c97be
Show file tree
Hide file tree
Showing 14 changed files with 141 additions and 78 deletions.
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

0 comments on commit 44c97be

Please sign in to comment.