Skip to content

Commit 961d04b

Browse files
authored
Merge pull request #2288 from plotly/nonuniform-bricks
Fix coloring and hover data for nonuniform heatmap & contour bricks
2 parents a775325 + 3581c87 commit 961d04b

File tree

7 files changed

+201
-100
lines changed

7 files changed

+201
-100
lines changed

src/traces/heatmap/calc.js

+14-6
Original file line numberDiff line numberDiff line change
@@ -115,19 +115,27 @@ module.exports = function calc(gd, trace) {
115115
}
116116

117117
// create arrays of brick boundaries, to be used by autorange and heatmap.plot
118-
var xlen = maxRowLength(z),
119-
xIn = trace.xtype === 'scaled' ? '' : x,
120-
xArray = makeBoundArray(trace, xIn, x0, dx, xlen, xa),
121-
yIn = trace.ytype === 'scaled' ? '' : y,
122-
yArray = makeBoundArray(trace, yIn, y0, dy, z.length, ya);
118+
var xlen = maxRowLength(z);
119+
var xIn = trace.xtype === 'scaled' ? '' : x;
120+
var xArray = makeBoundArray(trace, xIn, x0, dx, xlen, xa);
121+
var yIn = trace.ytype === 'scaled' ? '' : y;
122+
var yArray = makeBoundArray(trace, yIn, y0, dy, z.length, ya);
123123

124124
// handled in gl2d convert step
125125
if(!isGL2D) {
126126
Axes.expand(xa, xArray);
127127
Axes.expand(ya, yArray);
128128
}
129129

130-
var cd0 = {x: xArray, y: yArray, z: z, text: trace.text};
130+
var cd0 = {
131+
x: xArray,
132+
y: yArray,
133+
z: z,
134+
text: trace.text
135+
};
136+
137+
if(xIn && xIn.length === xArray.length - 1) cd0.xCenter = xIn;
138+
if(yIn && yIn.length === yArray.length - 1) cd0.yCenter = yIn;
131139

132140
if(isHist) {
133141
cd0.xRanges = binned.xRanges;

src/traces/heatmap/hover.js

+20-20
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,22 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, hoverLay
1919
// never let a heatmap override another type as closest point
2020
if(pointData.distance < MAXDIST) return;
2121

22-
var cd0 = pointData.cd[0],
23-
trace = cd0.trace,
24-
xa = pointData.xa,
25-
ya = pointData.ya,
26-
x = cd0.x,
27-
y = cd0.y,
28-
z = cd0.z,
29-
zmask = cd0.zmask,
30-
range = [trace.zmin, trace.zmax],
31-
zhoverformat = trace.zhoverformat,
32-
x2 = x,
33-
y2 = y,
34-
xl,
35-
yl,
36-
nx,
37-
ny;
22+
var cd0 = pointData.cd[0];
23+
var trace = cd0.trace;
24+
var xa = pointData.xa;
25+
var ya = pointData.ya;
26+
var x = cd0.x;
27+
var y = cd0.y;
28+
var z = cd0.z;
29+
var xc = cd0.xCenter;
30+
var yc = cd0.yCenter;
31+
var zmask = cd0.zmask;
32+
var range = [trace.zmin, trace.zmax];
33+
var zhoverformat = trace.zhoverformat;
34+
var x2 = x;
35+
var y2 = y;
36+
37+
var xl, yl, nx, ny;
3838

3939
if(pointData.index !== false) {
4040
try {
@@ -86,11 +86,11 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, hoverLay
8686
yl = y[ny];
8787
}
8888
else {
89-
xl = (x[nx] + x[nx + 1]) / 2;
90-
yl = (y[ny] + y[ny + 1]) / 2;
89+
xl = xc ? xc[nx] : ((x[nx] + x[nx + 1]) / 2);
90+
yl = yc ? yc[ny] : ((y[ny] + y[ny + 1]) / 2);
9191
if(trace.zsmooth) {
92-
x0 = x1 = (x0 + x1) / 2;
93-
y0 = y1 = (y0 + y1) / 2;
92+
x0 = x1 = xa.c2p(xl);
93+
y0 = y1 = ya.c2p(yl);
9494
}
9595
}
9696

src/traces/heatmap/plot.js

+96-70
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,14 @@ module.exports = function(gd, plotinfo, cdheatmaps) {
2525
}
2626
};
2727

28-
// From http://www.xarg.org/2010/03/generate-client-side-png-files-using-javascript/
2928
function plotOne(gd, plotinfo, cd) {
30-
var trace = cd[0].trace,
31-
uid = trace.uid,
32-
xa = plotinfo.xaxis,
33-
ya = plotinfo.yaxis,
34-
fullLayout = gd._fullLayout,
35-
id = 'hm' + uid;
29+
var cd0 = cd[0];
30+
var trace = cd0.trace;
31+
var uid = trace.uid;
32+
var xa = plotinfo.xaxis;
33+
var ya = plotinfo.yaxis;
34+
var fullLayout = gd._fullLayout;
35+
var id = 'hm' + uid;
3636

3737
// in case this used to be a contour map
3838
fullLayout._paper.selectAll('.contour' + uid).remove();
@@ -45,23 +45,21 @@ function plotOne(gd, plotinfo, cd) {
4545
return;
4646
}
4747

48-
var z = cd[0].z,
49-
x = cd[0].x,
50-
y = cd[0].y,
51-
isContour = Registry.traceIs(trace, 'contour'),
52-
zsmooth = isContour ? 'best' : trace.zsmooth,
53-
54-
// get z dims
55-
m = z.length,
56-
n = maxRowLength(z),
57-
xrev = false,
58-
left,
59-
right,
60-
temp,
61-
yrev = false,
62-
top,
63-
bottom,
64-
i;
48+
var z = cd0.z;
49+
var x = cd0.x;
50+
var y = cd0.y;
51+
var xc = cd0.xCenter;
52+
var yc = cd0.yCenter;
53+
var isContour = Registry.traceIs(trace, 'contour');
54+
var zsmooth = isContour ? 'best' : trace.zsmooth;
55+
56+
// get z dims
57+
var m = z.length;
58+
var n = maxRowLength(z);
59+
var xrev = false;
60+
var yrev = false;
61+
62+
var left, right, temp, top, bottom, i;
6563

6664
// TODO: if there are multiple overlapping categorical heatmaps,
6765
// or if we allow category sorting, then the categories may not be
@@ -113,11 +111,10 @@ function plotOne(gd, plotinfo, cd) {
113111
// for contours with heatmap fill, we generate the boundaries based on
114112
// brick centers but then use the brick edges for drawing the bricks
115113
if(isContour) {
116-
// TODO: for 'best' smoothing, we really should use the given brick
117-
// centers as well as brick bounds in calculating values, in case of
118-
// nonuniform brick sizes
119-
x = cd[0].xfill;
120-
y = cd[0].yfill;
114+
xc = x;
115+
yc = y;
116+
x = cd0.xfill;
117+
y = cd0.yfill;
121118
}
122119

123120
// make an image that goes at most half a screen off either side, to keep
@@ -199,30 +196,6 @@ function plotOne(gd, plotinfo, cd) {
199196
};
200197
}
201198

202-
// get interpolated bin value. Returns {bin0:closest bin, frac:fractional dist to next, bin1:next bin}
203-
function findInterp(pixel, pixArray) {
204-
var maxbin = pixArray.length - 2,
205-
bin = Lib.constrain(Lib.findBin(pixel, pixArray), 0, maxbin),
206-
pix0 = pixArray[bin],
207-
pix1 = pixArray[bin + 1],
208-
interp = Lib.constrain(bin + (pixel - pix0) / (pix1 - pix0) - 0.5, 0, maxbin),
209-
bin0 = Math.round(interp),
210-
frac = Math.abs(interp - bin0);
211-
212-
if(!interp || interp === maxbin || !frac) {
213-
return {
214-
bin0: bin0,
215-
bin1: bin0,
216-
frac: 0
217-
};
218-
}
219-
return {
220-
bin0: bin0,
221-
frac: frac,
222-
bin1: Math.round(bin0 + frac / (interp - bin0))
223-
};
224-
}
225-
226199
// build the pixel map brick-by-brick
227200
// cruise through z-matrix row-by-row
228201
// build a brick at each z-matrix value
@@ -254,13 +227,6 @@ function plotOne(gd, plotinfo, cd) {
254227
return [0, 0, 0, 0];
255228
}
256229

257-
function putColor(pixels, pxIndex, c) {
258-
pixels[pxIndex] = c[0];
259-
pixels[pxIndex + 1] = c[1];
260-
pixels[pxIndex + 2] = c[2];
261-
pixels[pxIndex + 3] = Math.round(c[3] * 255);
262-
}
263-
264230
function interpColor(r0, r1, xinterp, yinterp) {
265231
var z00 = r0[xinterp.bin0];
266232
if(z00 === undefined) return setColor(undefined, 1);
@@ -303,24 +269,26 @@ function plotOne(gd, plotinfo, cd) {
303269
}
304270

305271
if(zsmooth === 'best') {
306-
var xPixArray = new Array(x.length),
307-
yPixArray = new Array(y.length),
308-
xinterpArray = new Array(imageWidth),
309-
yinterp,
310-
r0,
311-
r1;
272+
var xForPx = xc || x;
273+
var yForPx = yc || y;
274+
var xPixArray = new Array(xForPx.length);
275+
var yPixArray = new Array(yForPx.length);
276+
var xinterpArray = new Array(imageWidth);
277+
var findInterpX = xc ? findInterpFromCenters : findInterp;
278+
var findInterpY = yc ? findInterpFromCenters : findInterp;
279+
var yinterp, r0, r1;
312280

313281
// first make arrays of x and y pixel locations of brick boundaries
314-
for(i = 0; i < x.length; i++) xPixArray[i] = Math.round(xa.c2p(x[i]) - left);
315-
for(i = 0; i < y.length; i++) yPixArray[i] = Math.round(ya.c2p(y[i]) - top);
282+
for(i = 0; i < xForPx.length; i++) xPixArray[i] = Math.round(xa.c2p(xForPx[i]) - left);
283+
for(i = 0; i < yForPx.length; i++) yPixArray[i] = Math.round(ya.c2p(yForPx[i]) - top);
316284

317285
// then make arrays of interpolations
318286
// (bin0=closest, bin1=next, frac=fractional dist.)
319-
for(i = 0; i < imageWidth; i++) xinterpArray[i] = findInterp(i, xPixArray);
287+
for(i = 0; i < imageWidth; i++) xinterpArray[i] = findInterpX(i, xPixArray);
320288

321289
// now do the interpolations and fill the png
322290
for(j = 0; j < imageHeight; j++) {
323-
yinterp = findInterp(j, yPixArray);
291+
yinterp = findInterpY(j, yPixArray);
324292
r0 = z[yinterp.bin0];
325293
r1 = z[yinterp.bin1];
326294
for(i = 0; i < imageWidth; i++, pxIndex += 4) {
@@ -415,3 +383,61 @@ function plotOne(gd, plotinfo, cd) {
415383

416384
image3.exit().remove();
417385
}
386+
387+
// get interpolated bin value. Returns {bin0:closest bin, frac:fractional dist to next, bin1:next bin}
388+
function findInterp(pixel, pixArray) {
389+
var maxBin = pixArray.length - 2;
390+
var bin = Lib.constrain(Lib.findBin(pixel, pixArray), 0, maxBin);
391+
var pix0 = pixArray[bin];
392+
var pix1 = pixArray[bin + 1];
393+
var interp = Lib.constrain(bin + (pixel - pix0) / (pix1 - pix0) - 0.5, 0, maxBin);
394+
var bin0 = Math.round(interp);
395+
var frac = Math.abs(interp - bin0);
396+
397+
if(!interp || interp === maxBin || !frac) {
398+
return {
399+
bin0: bin0,
400+
bin1: bin0,
401+
frac: 0
402+
};
403+
}
404+
return {
405+
bin0: bin0,
406+
frac: frac,
407+
bin1: Math.round(bin0 + frac / (interp - bin0))
408+
};
409+
}
410+
411+
function findInterpFromCenters(pixel, centerPixArray) {
412+
var maxBin = centerPixArray.length - 1;
413+
var bin = Lib.constrain(Lib.findBin(pixel, centerPixArray), 0, maxBin);
414+
var pix0 = centerPixArray[bin];
415+
var pix1 = centerPixArray[bin + 1];
416+
var frac = ((pixel - pix0) / (pix1 - pix0)) || 0;
417+
if(frac <= 0) {
418+
return {
419+
bin0: bin,
420+
bin1: bin,
421+
frac: 0
422+
};
423+
}
424+
if(frac < 0.5) {
425+
return {
426+
bin0: bin,
427+
bin1: bin + 1,
428+
frac: frac
429+
};
430+
}
431+
return {
432+
bin0: bin + 1,
433+
bin1: bin,
434+
frac: 1 - frac
435+
};
436+
}
437+
438+
function putColor(pixels, pxIndex, c) {
439+
pixels[pxIndex] = c[0];
440+
pixels[pxIndex + 1] = c[1];
441+
pixels[pxIndex + 2] = c[2];
442+
pixels[pxIndex + 3] = Math.round(c[3] * 255);
443+
}
Loading
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"data": [
3+
{
4+
"x": [0, 10, 12],
5+
"y": [10, 12, 0],
6+
"z": [1, 2, 3],
7+
"zsmooth": "best",
8+
"type": "heatmap",
9+
"colorscale": "Cividis",
10+
"colorbar": {"x": 0.42}
11+
},
12+
{
13+
"x": [0, 10, 12],
14+
"y": [10, 12, 0],
15+
"z": [1, 2, 3],
16+
"contours": {"coloring": "heatmap"},
17+
"line": {"color": "#fff"},
18+
"colorscale": "Cividis",
19+
"type": "contour",
20+
"xaxis": "x2",
21+
"yaxis": "y2"
22+
}
23+
],
24+
"layout": {
25+
"title": "heatmap and contour map with irregular bricks",
26+
"xaxis": {"domain": [0, 0.4]},
27+
"xaxis2": {"domain": [0.6, 1]},
28+
"yaxis2": {"anchor": "x2"},
29+
"height": 400,
30+
"width": 800
31+
}
32+
}

test/jasmine/tests/heatmap_test.js

+39-4
Original file line numberDiff line numberDiff line change
@@ -594,10 +594,10 @@ describe('heatmap hover', function() {
594594
}
595595

596596
function assertLabels(hoverPoint, xLabel, yLabel, zLabel, text) {
597-
expect(hoverPoint.xLabelVal).toEqual(xLabel, 'have correct x label');
598-
expect(hoverPoint.yLabelVal).toEqual(yLabel, 'have correct y label');
599-
expect(hoverPoint.zLabelVal).toEqual(zLabel, 'have correct z label');
600-
expect(hoverPoint.text).toEqual(text, 'have correct text label');
597+
expect(hoverPoint.xLabelVal).toBe(xLabel, 'have correct x label');
598+
expect(hoverPoint.yLabelVal).toBe(yLabel, 'have correct y label');
599+
expect(hoverPoint.zLabelVal).toBe(zLabel, 'have correct z label');
600+
expect(hoverPoint.text).toBe(text, 'have correct text label');
601601
}
602602

603603
describe('for `heatmap_multi-trace`', function() {
@@ -662,4 +662,39 @@ describe('heatmap hover', function() {
662662
});
663663

664664
});
665+
666+
describe('nonuniform bricks', function() {
667+
668+
beforeAll(function(done) {
669+
gd = createGraphDiv();
670+
671+
var mock = require('@mocks/heatmap_contour_irregular_bricks.json');
672+
var mockCopy = Lib.extendDeep({}, mock);
673+
674+
Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done);
675+
});
676+
677+
afterAll(destroyGraphDiv);
678+
679+
function checkData() {
680+
var pt = _hover(gd, -4, 6)[0];
681+
assertLabels(pt, 0, 10, 1);
682+
683+
pt = _hover(gd, 10.5, 12.5)[0];
684+
assertLabels(pt, 10, 12, 2);
685+
686+
pt = _hover(gd, 11.5, 4)[0];
687+
assertLabels(pt, 12, 0, 3);
688+
}
689+
690+
it('gives data positions, not brick centers', function(done) {
691+
checkData();
692+
693+
Plotly.restyle(gd, {zsmooth: 'none'}, [0])
694+
.then(checkData)
695+
.catch(failTest)
696+
.then(done);
697+
});
698+
699+
});
665700
});

0 commit comments

Comments
 (0)