Skip to content

Fix coloring and hover data for nonuniform heatmap & contour bricks #2288

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

Merged
merged 4 commits into from
Jan 24, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions src/traces/heatmap/calc.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,19 +115,27 @@ module.exports = function calc(gd, trace) {
}

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

// handled in gl2d convert step
if(!isGL2D) {
Axes.expand(xa, xArray);
Axes.expand(ya, yArray);
}

var cd0 = {x: xArray, y: yArray, z: z, text: trace.text};
var cd0 = {
x: xArray,
y: yArray,
z: z,
text: trace.text
};

if(xIn && xIn.length === xArray.length - 1) cd0.xCenter = xIn;
if(yIn && yIn.length === yArray.length - 1) cd0.yCenter = yIn;

if(isHist) {
cd0.xRanges = binned.xRanges;
Expand Down
40 changes: 20 additions & 20 deletions src/traces/heatmap/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,22 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, hoverLay
// never let a heatmap override another type as closest point
if(pointData.distance < MAXDIST) return;

var cd0 = pointData.cd[0],
trace = cd0.trace,
xa = pointData.xa,
ya = pointData.ya,
x = cd0.x,
y = cd0.y,
z = cd0.z,
zmask = cd0.zmask,
range = [trace.zmin, trace.zmax],
zhoverformat = trace.zhoverformat,
x2 = x,
y2 = y,
xl,
yl,
nx,
ny;
var cd0 = pointData.cd[0];
var trace = cd0.trace;
var xa = pointData.xa;
var ya = pointData.ya;
var x = cd0.x;
var y = cd0.y;
var z = cd0.z;
var xc = cd0.xCenter;
var yc = cd0.yCenter;
var zmask = cd0.zmask;
var range = [trace.zmin, trace.zmax];
var zhoverformat = trace.zhoverformat;
var x2 = x;
var y2 = y;

var xl, yl, nx, ny;

if(pointData.index !== false) {
try {
Expand Down Expand Up @@ -86,11 +86,11 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, hoverLay
yl = y[ny];
}
else {
xl = (x[nx] + x[nx + 1]) / 2;
yl = (y[ny] + y[ny + 1]) / 2;
xl = xc ? xc[nx] : ((x[nx] + x[nx + 1]) / 2);
yl = yc ? yc[ny] : ((y[ny] + y[ny + 1]) / 2);
if(trace.zsmooth) {
x0 = x1 = (x0 + x1) / 2;
y0 = y1 = (y0 + y1) / 2;
x0 = x1 = xa.c2p(xl);
y0 = y1 = ya.c2p(yl);
}
}

Expand Down
166 changes: 96 additions & 70 deletions src/traces/heatmap/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ module.exports = function(gd, plotinfo, cdheatmaps) {
}
};

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

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

var z = cd[0].z,
x = cd[0].x,
y = cd[0].y,
isContour = Registry.traceIs(trace, 'contour'),
zsmooth = isContour ? 'best' : trace.zsmooth,

// get z dims
m = z.length,
n = maxRowLength(z),
xrev = false,
left,
right,
temp,
yrev = false,
top,
bottom,
i;
var z = cd0.z;
var x = cd0.x;
var y = cd0.y;
var xc = cd0.xCenter;
var yc = cd0.yCenter;
var isContour = Registry.traceIs(trace, 'contour');
var zsmooth = isContour ? 'best' : trace.zsmooth;

// get z dims
var m = z.length;
var n = maxRowLength(z);
var xrev = false;
var yrev = false;

var left, right, temp, top, bottom, i;

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

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

// get interpolated bin value. Returns {bin0:closest bin, frac:fractional dist to next, bin1:next bin}
function findInterp(pixel, pixArray) {
var maxbin = pixArray.length - 2,
bin = Lib.constrain(Lib.findBin(pixel, pixArray), 0, maxbin),
pix0 = pixArray[bin],
pix1 = pixArray[bin + 1],
interp = Lib.constrain(bin + (pixel - pix0) / (pix1 - pix0) - 0.5, 0, maxbin),
bin0 = Math.round(interp),
frac = Math.abs(interp - bin0);

if(!interp || interp === maxbin || !frac) {
return {
bin0: bin0,
bin1: bin0,
frac: 0
};
}
return {
bin0: bin0,
frac: frac,
bin1: Math.round(bin0 + frac / (interp - bin0))
};
}

// build the pixel map brick-by-brick
// cruise through z-matrix row-by-row
// build a brick at each z-matrix value
Expand Down Expand Up @@ -254,13 +227,6 @@ function plotOne(gd, plotinfo, cd) {
return [0, 0, 0, 0];
}

function putColor(pixels, pxIndex, c) {
pixels[pxIndex] = c[0];
pixels[pxIndex + 1] = c[1];
pixels[pxIndex + 2] = c[2];
pixels[pxIndex + 3] = Math.round(c[3] * 255);
}

function interpColor(r0, r1, xinterp, yinterp) {
var z00 = r0[xinterp.bin0];
if(z00 === undefined) return setColor(undefined, 1);
Expand Down Expand Up @@ -303,24 +269,26 @@ function plotOne(gd, plotinfo, cd) {
}

if(zsmooth === 'best') {
var xPixArray = new Array(x.length),
yPixArray = new Array(y.length),
xinterpArray = new Array(imageWidth),
yinterp,
r0,
r1;
var xForPx = xc || x;
var yForPx = yc || y;
var xPixArray = new Array(xForPx.length);
var yPixArray = new Array(yForPx.length);
var xinterpArray = new Array(imageWidth);
var findInterpX = xc ? findInterpFromCenters : findInterp;
var findInterpY = yc ? findInterpFromCenters : findInterp;
var yinterp, r0, r1;

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

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

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

image3.exit().remove();
}

// get interpolated bin value. Returns {bin0:closest bin, frac:fractional dist to next, bin1:next bin}
function findInterp(pixel, pixArray) {
var maxBin = pixArray.length - 2;
var bin = Lib.constrain(Lib.findBin(pixel, pixArray), 0, maxBin);
var pix0 = pixArray[bin];
var pix1 = pixArray[bin + 1];
var interp = Lib.constrain(bin + (pixel - pix0) / (pix1 - pix0) - 0.5, 0, maxBin);
var bin0 = Math.round(interp);
var frac = Math.abs(interp - bin0);

if(!interp || interp === maxBin || !frac) {
return {
bin0: bin0,
bin1: bin0,
frac: 0
};
}
return {
bin0: bin0,
frac: frac,
bin1: Math.round(bin0 + frac / (interp - bin0))
};
}

function findInterpFromCenters(pixel, centerPixArray) {
var maxBin = centerPixArray.length - 1;
var bin = Lib.constrain(Lib.findBin(pixel, centerPixArray), 0, maxBin);
var pix0 = centerPixArray[bin];
var pix1 = centerPixArray[bin + 1];
var frac = ((pixel - pix0) / (pix1 - pix0)) || 0;
if(frac <= 0) {
return {
bin0: bin,
bin1: bin,
frac: 0
};
}
if(frac < 0.5) {
return {
bin0: bin,
bin1: bin + 1,
frac: frac
};
}
return {
bin0: bin + 1,
bin1: bin,
frac: 1 - frac
};
}

function putColor(pixels, pxIndex, c) {
pixels[pxIndex] = c[0];
pixels[pxIndex + 1] = c[1];
pixels[pxIndex + 2] = c[2];
pixels[pxIndex + 3] = Math.round(c[3] * 255);
}
Binary file modified test/image/baselines/contour_heatmap_coloring.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions test/image/mocks/heatmap_contour_irregular_bricks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"data": [
{
"x": [0, 10, 12],
"y": [10, 12, 0],
"z": [1, 2, 3],
"zsmooth": "best",
"type": "heatmap",
"colorscale": "Cividis",
"colorbar": {"x": 0.42}
},
{
"x": [0, 10, 12],
"y": [10, 12, 0],
"z": [1, 2, 3],
"contours": {"coloring": "heatmap"},
"line": {"color": "#fff"},
"colorscale": "Cividis",
"type": "contour",
"xaxis": "x2",
"yaxis": "y2"
}
],
"layout": {
"title": "heatmap and contour map with irregular bricks",
"xaxis": {"domain": [0, 0.4]},
"xaxis2": {"domain": [0.6, 1]},
"yaxis2": {"anchor": "x2"},
"height": 400,
"width": 800
}
}
43 changes: 39 additions & 4 deletions test/jasmine/tests/heatmap_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -594,10 +594,10 @@ describe('heatmap hover', function() {
}

function assertLabels(hoverPoint, xLabel, yLabel, zLabel, text) {
expect(hoverPoint.xLabelVal).toEqual(xLabel, 'have correct x label');
expect(hoverPoint.yLabelVal).toEqual(yLabel, 'have correct y label');
expect(hoverPoint.zLabelVal).toEqual(zLabel, 'have correct z label');
expect(hoverPoint.text).toEqual(text, 'have correct text label');
expect(hoverPoint.xLabelVal).toBe(xLabel, 'have correct x label');
expect(hoverPoint.yLabelVal).toBe(yLabel, 'have correct y label');
expect(hoverPoint.zLabelVal).toBe(zLabel, 'have correct z label');
expect(hoverPoint.text).toBe(text, 'have correct text label');
}

describe('for `heatmap_multi-trace`', function() {
Expand Down Expand Up @@ -662,4 +662,39 @@ describe('heatmap hover', function() {
});

});

describe('nonuniform bricks', function() {

beforeAll(function(done) {
gd = createGraphDiv();

var mock = require('@mocks/heatmap_contour_irregular_bricks.json');
var mockCopy = Lib.extendDeep({}, mock);

Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done);
});

afterAll(destroyGraphDiv);

function checkData() {
var pt = _hover(gd, -4, 6)[0];
assertLabels(pt, 0, 10, 1);

pt = _hover(gd, 10.5, 12.5)[0];
assertLabels(pt, 10, 12, 2);

pt = _hover(gd, 11.5, 4)[0];
assertLabels(pt, 12, 0, 3);
}

it('gives data positions, not brick centers', function(done) {
checkData();

Plotly.restyle(gd, {zsmooth: 'none'}, [0])
.then(checkData)
.catch(failTest)
.then(done);
});

});
});