Skip to content

Commit ab756a1

Browse files
committed
Improve heatmap rendering performance when zsmooth is false
- add `trace._is_linear` flag to help determine drawing method - use "fast" drawing method whenever possible (ie. flag is true, no gap) - fix heatmap tests, taking account of these changes
1 parent 55dda47 commit ab756a1

File tree

3 files changed

+62
-58
lines changed

3 files changed

+62
-58
lines changed

src/traces/heatmap/calc.js

Lines changed: 22 additions & 23 deletions
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) || !scaleIsLinear(y)) {
113+
if(zsmooth === 'fast') {
114+
noZsmooth('x/y scale is not linear');
115+
}
116+
} else {
117+
trace._islinear = true;
119118
}
120119

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

src/traces/heatmap/plot.js

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,18 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
109109
y = cd0.yfill;
110110
}
111111

112+
var drawingMethod = 'default';
113+
if(zsmooth) {
114+
drawingMethod = zsmooth === 'best' ? 'smooth' : 'fast';
115+
} else if(trace._islinear && xGap === 0 && yGap === 0) {
116+
drawingMethod = 'fast';
117+
}
118+
112119
// 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
120+
// time reasonable when you zoom in. if drawingMethod is fast, don't worry
114121
// about this, because zooming doesn't increase number of pixels
115122
// if zsmooth is best, don't include anything off screen because it takes too long
116-
if(zsmooth !== 'fast') {
123+
if(drawingMethod !== 'fast') {
117124
var extra = zsmooth === 'best' ? 0 : 0.5;
118125
left = Math.max(-extra * xa._length, left);
119126
right = Math.min((1 + extra) * xa._length, right);
@@ -140,7 +147,7 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
140147
// generate image data
141148

142149
var canvasW, canvasH;
143-
if(zsmooth === 'fast') {
150+
if(drawingMethod === 'fast') {
144151
canvasW = n;
145152
canvasH = m;
146153
} else {
@@ -158,7 +165,7 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
158165
// map brick boundaries to image pixels
159166
var xpx,
160167
ypx;
161-
if(zsmooth === 'fast') {
168+
if(drawingMethod === 'fast') {
162169
xpx = xrev ?
163170
function(index) { return n - 1 - index; } :
164171
Lib.identity;
@@ -235,7 +242,7 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
235242
return setColor(z00 + xinterp.frac * dx + yinterp.frac * (dy + xinterp.frac * dxy));
236243
}
237244

238-
if(zsmooth) { // best or fast, works fastest with imageData
245+
if(drawingMethod !== 'default') { // works fastest with imageData
239246
var pxIndex = 0;
240247
var pixels;
241248

@@ -245,7 +252,7 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
245252
pixels = new Array(canvasW * canvasH * 4);
246253
}
247254

248-
if(zsmooth === 'best') {
255+
if(drawingMethod === 'smooth') { // zsmooth="best"
249256
var xForPx = xc || x;
250257
var yForPx = yc || y;
251258
var xPixArray = new Array(xForPx.length);
@@ -273,7 +280,7 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
273280
putColor(pixels, pxIndex, c);
274281
}
275282
}
276-
} else { // zsmooth = fast
283+
} else { // drawingMethod = "fast" (zsmooth = "fast"|false)
277284
for(j = 0; j < m; j++) {
278285
row = z[j];
279286
yb = ypx(j);
@@ -297,7 +304,8 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
297304
}
298305

299306
context.putImageData(imageData, 0, 0);
300-
} else { // zsmooth = false -> filling potentially large bricks works fastest with fillRect
307+
} else { // rawingMethod = "default" (zsmooth = false)
308+
// filling potentially large bricks works fastest with fillRect
301309
// gaps do not need to be exact integers, but if they *are* we will get
302310
// cleaner edges by rounding at least one edge
303311
var xGapLeft = Math.floor(xGap / 2);
@@ -350,6 +358,7 @@ module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
350358
width: imageWidth,
351359
x: left,
352360
y: top,
361+
'image-rendering': drawingMethod === 'fast' && !zsmooth ? 'pixelated' : 'auto',
353362
'xlink:href': canvas.toDataURL('image/png')
354363
});
355364

test/jasmine/tests/heatmap_test.js

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ describe('heatmap supplyDefaults', function() {
7575
expect(traceOut.visible).toBe(false);
7676
});
7777

78-
it('should set visible to false when z isn\'t column not a 2d array', function() {
78+
it('should set visible to false when z isn\'t column nor a 2d array', function() {
7979
traceIn = {
8080
x: [1, 1, 1, 2, 2],
8181
y: [1, 2, 3, 1, 2],
@@ -683,7 +683,7 @@ describe('heatmap plot', function() {
683683

684684
return Plotly.relayout(gd, 'xaxis.range', [2, 3]);
685685
}).then(function() {
686-
assertImageCnt(2);
686+
assertImageCnt(3);
687687

688688
return Plotly.relayout(gd, 'xaxis.autorange', true);
689689
}).then(function() {
@@ -763,8 +763,12 @@ describe('heatmap plot', function() {
763763
};
764764
var originalCreateElement = document.createElement;
765765

766-
mockWithoutPadding.data[0].xgap = 0;
767-
mockWithoutPadding.data[0].ygap = 0;
766+
// We actually need to set a non-zero gap to ensure both mockWithPadding
767+
// and mockWithoutPadding relies on the same drawing method (ie. default
768+
// method using fillRect)
769+
var nearZeroGap = 0.1;
770+
mockWithoutPadding.data[0].xgap = nearZeroGap;
771+
mockWithoutPadding.data[0].ygap = nearZeroGap;
768772

769773
spyOn(document, 'createElement').and.callFake(function(elementType) {
770774
var element = originalCreateElement.call(document, elementType);
@@ -782,8 +786,8 @@ describe('heatmap plot', function() {
782786
}).then(function() {
783787
var xGap = mockWithPadding.data[0].xgap;
784788
var yGap = mockWithPadding.data[0].ygap;
785-
var xGapLeft = xGap / 2;
786-
var yGapTop = yGap / 2;
789+
var xGapLeft = Math.floor(xGap / 2);
790+
var yGapTop = Math.floor(yGap / 2);
787791

788792
argumentsWithPadding = getContextStub.fillRect.calls.allArgs()
789793
.slice(getContextStub.fillRect.calls.allArgs().length - 25);
@@ -793,8 +797,8 @@ describe('heatmap plot', function() {
793797
argumentsWithPadding.forEach(function(args, i) {
794798
expect(args[0]).toBe(argumentsWithoutPadding[i][0] + xGapLeft, i);
795799
expect(args[1]).toBe(argumentsWithoutPadding[i][1] + yGapTop, i);
796-
expect(args[2]).toBe(argumentsWithoutPadding[i][2] - xGap, i);
797-
expect(args[3]).toBe(argumentsWithoutPadding[i][3] - yGap, i);
800+
expect(args[2]).toBe(argumentsWithoutPadding[i][2] + nearZeroGap - xGap, i);
801+
expect(args[3]).toBe(argumentsWithoutPadding[i][3] + nearZeroGap - yGap, i);
798802
});
799803
})
800804
.then(done, done.fail);
@@ -827,7 +831,7 @@ describe('heatmap plot', function() {
827831
.then(done, done.fail);
828832
});
829833

830-
it('should set canvas dimensions according to z data shape if `zsmooth` is fast', function(done) {
834+
it('should set canvas dimensions according to z data shape when using fast drawing method', function(done) {
831835
var mock1 = require('../../image/mocks/zsmooth_methods.json');
832836
var mock2 = require('../../image/mocks/heatmap_small_layout_zsmooth_fast.json');
833837

@@ -863,7 +867,7 @@ describe('heatmap plot', function() {
863867
}).then(done, done.fail);
864868
});
865869

866-
it('should create imageData that fits the canvas dimensions if zsmooth is set', function(done) {
870+
it('should create imageData that fits the canvas dimensions if zsmooth is set and/or drawing method is fast', function(done) {
867871
var mock1 = require('../../image/mocks/zsmooth_methods.json');
868872
var mock2 = require('../../image/mocks/heatmap_small_layout_zsmooth_fast.json');
869873

@@ -905,11 +909,11 @@ describe('heatmap plot', function() {
905909
return element;
906910
});
907911

908-
Plotly.newPlot(gd, mock1.data, mock1.layout).then(function() {
909-
expect(getContextStub.createImageData.calls.count()).toBe(2);
910-
expect(imageDataStub.data.set.calls.count()).toBe(2);
912+
function assertImageData(traceIndices) {
913+
expect(getContextStub.createImageData.calls.count()).toBe(traceIndices.length);
914+
expect(imageDataStub.data.set.calls.count()).toBe(traceIndices.length);
911915

912-
[0, 1].forEach(function(i) {
916+
traceIndices.forEach(function(i) {
913917
var createImageDataArgs = getContextStub.createImageData.calls.argsFor(i);
914918
var setImageDataArgs = imageDataStub.data.set.calls.argsFor(i);
915919

@@ -921,26 +925,18 @@ describe('heatmap plot', function() {
921925
expect(pixels.length).toBe(canvasW * canvasH * 4);
922926
expect(checkPixels(pixels)).toBe(true);
923927
});
928+
}
929+
930+
Plotly.newPlot(gd, mock1.data, mock1.layout).then(function() {
931+
assertImageData([0, 1, 2]);
924932

925933
getContextStub.createImageData.calls.reset();
926934
imageDataStub.data.set.calls.reset();
927935
canvasStubs = [];
928936

929937
return Plotly.newPlot(gd, mock2.data, mock2.layout);
930938
}).then(function() {
931-
expect(getContextStub.createImageData.calls.count()).toBe(1);
932-
expect(imageDataStub.data.set.calls.count()).toBe(1);
933-
934-
var canvasW = canvasStubs[0].width.calls.argsFor(0)[0];
935-
var canvasH = canvasStubs[0].height.calls.argsFor(0)[0];
936-
937-
var createImageDataArgs = getContextStub.createImageData.calls.argsFor(0);
938-
expect(createImageDataArgs).toEqual([canvasW, canvasH]);
939-
940-
var setImageDataArgs = imageDataStub.data.set.calls.argsFor(0);
941-
var pixels = setImageDataArgs[0];
942-
expect(pixels.length).toBe(canvasW * canvasH * 4);
943-
expect(checkPixels(pixels)).toBe(true);
939+
assertImageData([0]);
944940
}).then(done, done.fail);
945941
});
946942
});

0 commit comments

Comments
 (0)