Skip to content

Commit 44c97be

Browse files
authored
Merge pull request #6574 from lvlte/heatmap_rendering_perf
Improve heatmap rendering performance when `zsmooth` is false
2 parents a4ce2b7 + 4eaf62e commit 44c97be

14 files changed

+141
-78
lines changed

Diff for: draftlogs/6574_change.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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!

Diff for: src/constants/pixelated_image.js

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
'use strict';
2+
3+
// Pixelated image rendering
4+
// The actual CSS declaration is prepended with fallbacks for older browsers.
5+
// NB. IE's `-ms-interpolation-mode` works only with <img> not with SVG <image>
6+
// https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering
7+
// https://caniuse.com/?search=image-rendering
8+
// http://phrogz.net/tmp/canvas_image_zoom.html
9+
10+
exports.CSS_DECLARATIONS = [
11+
['image-rendering', 'optimizeSpeed'],
12+
['image-rendering', '-moz-crisp-edges'],
13+
['image-rendering', '-o-crisp-edges'],
14+
['image-rendering', '-webkit-optimize-contrast'],
15+
['image-rendering', 'optimize-contrast'],
16+
['image-rendering', 'crisp-edges'],
17+
['image-rendering', 'pixelated']
18+
];
19+
20+
exports.STYLE = exports.CSS_DECLARATIONS.map(function(d) {
21+
return d.join(': ') + '; ';
22+
}).join('');

Diff for: src/lib/supports_pixelated_image.js

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
'use strict';
2+
3+
var constants = require('../constants/pixelated_image');
4+
var Drawing = require('../components/drawing');
5+
var Lib = require('../lib');
6+
7+
var _supportsPixelated = null;
8+
9+
/**
10+
* Check browser support for pixelated image rendering
11+
*
12+
* @return {boolean}
13+
*/
14+
function supportsPixelatedImage() {
15+
if(_supportsPixelated !== null) { // only run the feature detection once
16+
return _supportsPixelated;
17+
}
18+
if(Lib.isIE()) {
19+
_supportsPixelated = false;
20+
} else {
21+
var declarations = Array.from(constants.CSS_DECLARATIONS).reverse();
22+
var supports = window.CSS && window.CSS.supports || window.supportsCSS;
23+
if(typeof supports === 'function') {
24+
_supportsPixelated = declarations.some(function(d) {
25+
return supports.apply(null, d);
26+
});
27+
} else {
28+
var image3 = Drawing.tester.append('image');
29+
var cStyles = window.getComputedStyle(image3.node());
30+
image3.attr('style', constants.STYLE);
31+
_supportsPixelated = declarations.some(function(d) {
32+
var value = d[1];
33+
return cStyles.imageRendering === value ||
34+
cStyles.imageRendering === value.toLowerCase();
35+
});
36+
image3.remove();
37+
}
38+
}
39+
return _supportsPixelated;
40+
}
41+
42+
module.exports = supportsPixelatedImage;

Diff for: src/traces/heatmap/calc.js

+22-23
Original file line numberDiff line numberDiff line change
@@ -90,32 +90,31 @@ module.exports = function calc(gd, trace) {
9090
Lib.warn('cannot use zsmooth: "fast": ' + msg);
9191
}
9292

93-
// check whether we really can smooth (ie all boxes are about the same size)
94-
if(zsmooth === 'fast') {
95-
if(xa.type === 'log' || ya.type === 'log') {
96-
noZsmooth('log axis found');
97-
} else if(!isHist) {
98-
if(x.length) {
99-
var avgdx = (x[x.length - 1] - x[0]) / (x.length - 1);
100-
var maxErrX = Math.abs(avgdx / 100);
101-
for(i = 0; i < x.length - 1; i++) {
102-
if(Math.abs(x[i + 1] - x[i] - avgdx) > maxErrX) {
103-
noZsmooth('x scale is not linear');
104-
break;
105-
}
106-
}
107-
}
108-
if(y.length && zsmooth === 'fast') {
109-
var avgdy = (y[y.length - 1] - y[0]) / (y.length - 1);
110-
var maxErrY = Math.abs(avgdy / 100);
111-
for(i = 0; i < y.length - 1; i++) {
112-
if(Math.abs(y[i + 1] - y[i] - avgdy) > maxErrY) {
113-
noZsmooth('y scale is not linear');
114-
break;
115-
}
93+
function scaleIsLinear(s) {
94+
if(s.length > 1) {
95+
var avgdx = (s[s.length - 1] - s[0]) / (s.length - 1);
96+
var maxErrX = Math.abs(avgdx / 100);
97+
for(i = 0; i < s.length - 1; i++) {
98+
if(Math.abs(s[i + 1] - s[i] - avgdx) > maxErrX) {
99+
return false;
116100
}
117101
}
118102
}
103+
return true;
104+
}
105+
106+
// Check whether all brick are uniform
107+
trace._islinear = false;
108+
if(xa.type === 'log' || ya.type === 'log') {
109+
if(zsmooth === 'fast') {
110+
noZsmooth('log axis found');
111+
}
112+
} else if(!scaleIsLinear(x)) {
113+
if(zsmooth === 'fast') noZsmooth('x scale is not linear');
114+
} else if(!scaleIsLinear(y)) {
115+
if(zsmooth === 'fast') noZsmooth('y scale is not linear');
116+
} else {
117+
trace._islinear = true;
119118
}
120119

121120
// create arrays of brick boundaries, to be used by autorange and heatmap.plot

Diff for: src/traces/heatmap/plot.js

+25-9
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ var makeColorScaleFuncFromTrace = require('../../components/colorscale').makeCol
1515
var xmlnsNamespaces = require('../../constants/xmlns_namespaces');
1616
var alignmentConstants = require('../../constants/alignment');
1717
var LINE_SPACING = alignmentConstants.LINE_SPACING;
18+
var supportsPixelatedImage = require('../../lib/supports_pixelated_image');
19+
var PIXELATED_IMAGE_STYLE = require('../../constants/pixelated_image').STYLE;
1820

1921
var labelClass = 'heatmap-label';
2022

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

114+
var drawingMethod = 'default';
115+
if(zsmooth) {
116+
drawingMethod = zsmooth === 'best' ? 'smooth' : 'fast';
117+
} else if(trace._islinear && xGap === 0 && yGap === 0 && supportsPixelatedImage()) {
118+
drawingMethod = 'fast';
119+
}
120+
112121
// make an image that goes at most half a screen off either side, to keep
113-
// time reasonable when you zoom in. if zsmooth is true/fast, don't worry
122+
// time reasonable when you zoom in. if drawingMethod is fast, don't worry
114123
// about this, because zooming doesn't increase number of pixels
115124
// if zsmooth is best, don't include anything off screen because it takes too long
116-
if(zsmooth !== 'fast') {
125+
if(drawingMethod !== 'fast') {
117126
var extra = zsmooth === 'best' ? 0 : 0.5;
118127
left = Math.max(-extra * xa._length, left);
119128
right = Math.min((1 + extra) * xa._length, right);
@@ -127,7 +136,9 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
127136
// setup image nodes
128137

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

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

142153
var canvasW, canvasH;
143-
if(zsmooth === 'fast') {
154+
if(drawingMethod === 'fast') {
144155
canvasW = n;
145156
canvasH = m;
146157
} else {
@@ -158,7 +169,7 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
158169
// map brick boundaries to image pixels
159170
var xpx,
160171
ypx;
161-
if(zsmooth === 'fast') {
172+
if(drawingMethod === 'fast') {
162173
xpx = xrev ?
163174
function(index) { return n - 1 - index; } :
164175
Lib.identity;
@@ -235,7 +246,7 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
235246
return setColor(z00 + xinterp.frac * dx + yinterp.frac * (dy + xinterp.frac * dxy));
236247
}
237248

238-
if(zsmooth) { // best or fast, works fastest with imageData
249+
if(drawingMethod !== 'default') { // works fastest with imageData
239250
var pxIndex = 0;
240251
var pixels;
241252

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

248-
if(zsmooth === 'best') {
259+
if(drawingMethod === 'smooth') { // zsmooth="best"
249260
var xForPx = xc || x;
250261
var yForPx = yc || y;
251262
var xPixArray = new Array(xForPx.length);
@@ -273,7 +284,7 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
273284
putColor(pixels, pxIndex, c);
274285
}
275286
}
276-
} else { // zsmooth = fast
287+
} else { // drawingMethod = "fast" (zsmooth = "fast"|false)
277288
for(j = 0; j < m; j++) {
278289
row = z[j];
279290
yb = ypx(j);
@@ -297,7 +308,8 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
297308
}
298309

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

368+
if(drawingMethod === 'fast' && !zsmooth) {
369+
image3.attr('style', PIXELATED_IMAGE_STYLE);
370+
}
371+
356372
removeLabels(plotGroup);
357373

358374
var texttemplate = trace.texttemplate;

Diff for: src/traces/image/constants.js

+1-14
Original file line numberDiff line numberDiff line change
@@ -47,18 +47,5 @@ module.exports = {
4747
},
4848
suffix: ['°', '%', '%', '']
4949
}
50-
},
51-
// For pixelated image rendering
52-
// http://phrogz.net/tmp/canvas_image_zoom.html
53-
// https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering
54-
pixelatedStyle: [
55-
'image-rendering: optimizeSpeed',
56-
'image-rendering: -moz-crisp-edges',
57-
'image-rendering: -o-crisp-edges',
58-
'image-rendering: -webkit-optimize-contrast',
59-
'image-rendering: optimize-contrast',
60-
'image-rendering: crisp-edges',
61-
'image-rendering: pixelated',
62-
''
63-
].join('; ')
50+
}
6451
};

Diff for: src/traces/image/plot.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,21 @@ var Lib = require('../../lib');
55
var strTranslate = Lib.strTranslate;
66
var xmlnsNamespaces = require('../../constants/xmlns_namespaces');
77
var constants = require('./constants');
8-
9-
var unsupportedBrowsers = Lib.isIOS() || Lib.isSafari() || Lib.isIE();
8+
var supportsPixelatedImage = require('../../lib/supports_pixelated_image');
9+
var PIXELATED_IMAGE_STYLE = require('../../constants/pixelated_image').STYLE;
1010

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

15-
var supportsPixelatedImage = !(unsupportedBrowsers || gd._context._exportedPlot);
15+
var supportsPixelated = !gd._context._exportedPlot && supportsPixelatedImage();
1616

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

132132
image3.exit().remove();
133133

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

136136
if(realImage) {
137137
var xRange = Lib.simpleMap(xa.range, xa.r2l);

Diff for: tasks/test_syntax.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ function assertSrcContents() {
193193
* - If you use conforms to these rules, you may update
194194
* KNOWN_GET_COMPUTED_STYLE_CALLS to count the new use.
195195
*/
196-
var KNOWN_GET_COMPUTED_STYLE_CALLS = 6;
196+
var KNOWN_GET_COMPUTED_STYLE_CALLS = 7;
197197
if(getComputedStyleCnt !== KNOWN_GET_COMPUTED_STYLE_CALLS) {
198198
logs.push('Expected ' + KNOWN_GET_COMPUTED_STYLE_CALLS +
199199
' window.getComputedStyle calls, found ' + getComputedStyleCnt +

Diff for: test/image/baselines/cmid-zmid.png

-121 Bytes
Loading

Diff for: test/image/baselines/dendrogram.png

-1.26 KB
Loading

Diff for: test/image/baselines/heatmap_multicategory.png

-104 Bytes
Loading
14 Bytes
Loading

Diff for: test/image/baselines/updatemenus.png

-25 Bytes
Loading

0 commit comments

Comments
 (0)