diff --git a/src/traces/heatmap/calc.js b/src/traces/heatmap/calc.js index c3b21f96070..ca8bba0e20c 100644 --- a/src/traces/heatmap/calc.js +++ b/src/traces/heatmap/calc.js @@ -115,11 +115,11 @@ 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) { @@ -127,7 +127,15 @@ module.exports = function calc(gd, trace) { 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; diff --git a/src/traces/heatmap/hover.js b/src/traces/heatmap/hover.js index 704c93a3718..7fc667616ac 100644 --- a/src/traces/heatmap/hover.js +++ b/src/traces/heatmap/hover.js @@ -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 { @@ -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); } } diff --git a/src/traces/heatmap/plot.js b/src/traces/heatmap/plot.js index df8585c90e7..00212e4bd5e 100644 --- a/src/traces/heatmap/plot.js +++ b/src/traces/heatmap/plot.js @@ -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(); @@ -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 @@ -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 @@ -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 @@ -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); @@ -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) { @@ -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); +} diff --git a/test/image/baselines/contour_heatmap_coloring.png b/test/image/baselines/contour_heatmap_coloring.png index dd408d1120a..0e2a7370fcb 100644 Binary files a/test/image/baselines/contour_heatmap_coloring.png and b/test/image/baselines/contour_heatmap_coloring.png differ diff --git a/test/image/baselines/heatmap_contour_irregular_bricks.png b/test/image/baselines/heatmap_contour_irregular_bricks.png new file mode 100644 index 00000000000..84a3a9ccb85 Binary files /dev/null and b/test/image/baselines/heatmap_contour_irregular_bricks.png differ diff --git a/test/image/mocks/heatmap_contour_irregular_bricks.json b/test/image/mocks/heatmap_contour_irregular_bricks.json new file mode 100644 index 00000000000..b6a503f2318 --- /dev/null +++ b/test/image/mocks/heatmap_contour_irregular_bricks.json @@ -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 + } +} diff --git a/test/jasmine/tests/heatmap_test.js b/test/jasmine/tests/heatmap_test.js index 1f0afd929c0..30da55e7423 100644 --- a/test/jasmine/tests/heatmap_test.js +++ b/test/jasmine/tests/heatmap_test.js @@ -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() { @@ -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); + }); + + }); });