diff --git a/src/traces/heatmap/hover.js b/src/traces/heatmap/hover.js index 64bab8457b2..828a9033066 100644 --- a/src/traces/heatmap/hover.js +++ b/src/traces/heatmap/hover.js @@ -12,10 +12,12 @@ var Fx = require('../../plots/cartesian/graph_interact'); var Lib = require('../../lib'); +var MAXDIST = require('../../plots/cartesian/constants').MAXDIST; + module.exports = function hoverPoints(pointData, xval, yval, hovermode, contour) { // never let a heatmap override another type as closest point - if(pointData.distance < Fx.MAXDIST) return; + if(pointData.distance < MAXDIST) return; var cd0 = pointData.cd[0], trace = cd0.trace, @@ -46,8 +48,8 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, contour) return; } } - else if(Fx.inbox(xval - x[0], xval - x[x.length - 1]) > Fx.MAXDIST || - Fx.inbox(yval - y[0], yval - y[y.length - 1]) > Fx.MAXDIST) { + else if(Fx.inbox(xval - x[0], xval - x[x.length - 1]) > MAXDIST || + Fx.inbox(yval - y[0], yval - y[y.length - 1]) > MAXDIST) { return; } else { @@ -69,10 +71,12 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, contour) nx = Math.max(0, Math.min(x2.length - 2, Lib.findBin(xval, x2))); ny = Math.max(0, Math.min(y2.length - 2, Lib.findBin(yval, y2))); } + var x0 = xa.c2p(x[nx]), x1 = xa.c2p(x[nx + 1]), y0 = ya.c2p(y[ny]), y1 = ya.c2p(y[ny + 1]); + if(contour) { x1 = x0; xl = x[nx]; @@ -99,7 +103,7 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, contour) return [Lib.extendFlat(pointData, { index: [ny, nx], // never let a 2D override 1D type as closest point - distance: Fx.MAXDIST + 10, + distance: MAXDIST + 10, x0: x0, x1: x1, y0: y0, diff --git a/src/traces/heatmap/plot.js b/src/traces/heatmap/plot.js index 93a721f1fb1..d2710bded24 100644 --- a/src/traces/heatmap/plot.js +++ b/src/traces/heatmap/plot.js @@ -137,10 +137,24 @@ function plotOne(gd, plotinfo, cd) { var imageWidth = Math.round(right - left), imageHeight = Math.round(bottom - top); - // now redraw + // setup image nodes // if image is entirely off-screen, don't even draw it - if(imageWidth <= 0 || imageHeight <= 0) return; + var isOffScreen = (imageWidth <= 0 || imageHeight <= 0); + + var plotgroup = plotinfo.plot.select('.imagelayer') + .selectAll('g.hm.' + id) + .data(isOffScreen ? [] : [0]); + + plotgroup.enter().append('g') + .classed('hm', true) + .classed(id, true); + + plotgroup.exit().remove(); + + if(isOffScreen) return; + + // generate image data var canvasW, canvasH; if(zsmooth === 'fast') { @@ -363,26 +377,21 @@ function plotOne(gd, plotinfo, cd) { gd._hmpixcount = (gd._hmpixcount||0) + pixcount; gd._hmlumcount = (gd._hmlumcount||0) + pixcount * avgColor.getLuminance(); - var plotgroup = plotinfo.plot.select('.imagelayer') - .selectAll('g.hm.' + id) - .data([0]); - plotgroup.enter().append('g') - .classed('hm', true) - .classed(id, true); - plotgroup.exit().remove(); - var image3 = plotgroup.selectAll('image') .data(cd); - image3.enter().append('svg:image'); - image3.exit().remove(); - image3.attr({ + image3.enter().append('svg:image').attr({ xmlns: xmlnsNamespaces.svg, 'xlink:href': canvas.toDataURL('image/png'), + preserveAspectRatio: 'none' + }); + + image3.attr({ height: imageHeight, width: imageWidth, x: left, - y: top, - preserveAspectRatio: 'none' + y: top }); + + image3.exit().remove(); } diff --git a/test/image/baselines/heatmap_multi-trace.png b/test/image/baselines/heatmap_multi-trace.png new file mode 100644 index 00000000000..27ab01075b4 Binary files /dev/null and b/test/image/baselines/heatmap_multi-trace.png differ diff --git a/test/image/mocks/heatmap_multi-trace.json b/test/image/mocks/heatmap_multi-trace.json new file mode 100644 index 00000000000..4bd6f5d065b --- /dev/null +++ b/test/image/mocks/heatmap_multi-trace.json @@ -0,0 +1,193 @@ +{ + "data": [ + { + "x": [ + 1, + 1, + 1, + 1, + 1, + 1 + ], + "y": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "z": [ + 3, + 4, + 2, + 5, + 3, + 1 + ], + "type": "heatmap", + "showlegend": false, + "showscale": false, + "zmax": 5, + "zmin": 1 + }, + { + "x": [ + 2, + 2, + 2, + 2, + 2 + ], + "y": [ + 0.2, + 1.2, + 2.2, + 3.2, + 4.2 + ], + "z": [ + 6, + 1, + 3, + 2, + 4 + ], + "type": "heatmap", + "showlegend": false, + "showscale": false, + "zmax": 6, + "zmin": 1 + }, + { + "x": [ + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3 + ], + "y": [ + 0, + 0.5, + 1, + 1.5, + 2, + 2.5, + 3, + 3.5, + 4, + 4.5, + 5 + ], + "z": [ + 6, + 1, + 3, + 2, + 4, + 2, + 7, + 4, + 3, + 2, + 5 + ], + "type": "heatmap", + "showlegend": false, + "showscale": false, + "zmax": 7, + "zmin": 1 + }, + { + "x": [ + 4, + 4, + 4, + 4, + 4, + 4 + ], + "y": [ + 0, + 0.2, + 0.5, + 1.2, + 3.5, + 4 + ], + "z": [ + 2, + 4, + 7, + 3, + 2, + 1 + ], + "type": "heatmap", + "showlegend": false, + "showscale": false, + "zmax": 7, + "zmin": 1 + }, + { + "x": [ + 5.2, + 5.2, + 5.2, + 5.2, + 5.2, + 5.2 + ], + "y": [ + 0, + 0.2, + 0.5, + 1.2, + 3.5, + 4 + ], + "z": [ + 2, + 4, + 7, + 3, + 2, + 1 + ], + "type": "heatmap", + "showlegend": false, + "showscale": true, + "zmax": 10, + "zmin": 0 + } + ], + "layout": { + "hovermode": "closest", + "xaxis": { + "type": "linear", + "range": [ + 0.5, + 5.7 + ], + "autorange": true + }, + "yaxis": { + "type": "linear", + "range": [ + -0.5, + 5.5 + ], + "autorange": true + }, + "height": 450, + "width": 1100, + "autosize": true + } +} diff --git a/test/jasmine/tests/heatmap_test.js b/test/jasmine/tests/heatmap_test.js index dae549a81b0..32c6aea150a 100644 --- a/test/jasmine/tests/heatmap_test.js +++ b/test/jasmine/tests/heatmap_test.js @@ -1,9 +1,13 @@ +var Plotly = require('@lib/index'); var Plots = require('@src/plots/plots'); var Lib = require('@src/lib'); var convertColumnXYZ = require('@src/traces/heatmap/convert_column_xyz'); var Heatmap = require('@src/traces/heatmap'); +var d3 = require('d3'); +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); var customMatchers = require('../assets/custom_matchers'); @@ -299,3 +303,97 @@ describe('heatmap calc', function() { expect(out.z).toBeCloseTo2DArray([[1, 2, 3], [3, 1, 2]]); }); }); + +describe('heatmap plot', function() { + 'use strict'; + + afterEach(destroyGraphDiv); + + it('should not draw traces that are off-screen', function(done) { + var mock = require('@mocks/heatmap_multi-trace.json'), + mockCopy = Lib.extendDeep({}, mock), + gd = createGraphDiv(); + + function assertImageCnt(cnt) { + var images = d3.selectAll('.hm').select('image'); + + expect(images.size()).toEqual(cnt); + } + + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { + assertImageCnt(5); + + return Plotly.relayout(gd, 'xaxis.range', [2, 3]); + }).then(function() { + assertImageCnt(2); + + return Plotly.relayout(gd, 'xaxis.autorange', true); + }).then(function() { + assertImageCnt(5); + + done(); + }); + }); +}); + +describe('heatmap hover', function() { + 'use strict'; + + var gd; + + beforeAll(function(done) { + jasmine.addMatchers(customMatchers); + + gd = createGraphDiv(); + + var mock = require('@mocks/heatmap_multi-trace.json'), + mockCopy = Lib.extendDeep({}, mock); + + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); + + afterAll(destroyGraphDiv); + + function _hover(gd, xval, yval) { + var fullLayout = gd._fullLayout, + calcData = gd.calcdata, + hoverData = []; + + for(var i = 0; i < calcData.length; i++) { + var pointData = { + index: false, + distance: 20, + cd: calcData[i], + trace: calcData[i][0].trace, + xa: fullLayout.xaxis, + ya: fullLayout.yaxis + }; + + var hoverPoint = Heatmap.hoverPoints(pointData, xval, yval); + if(hoverPoint) hoverData.push(hoverPoint[0]); + } + + return hoverData; + } + + function assertLabels(hoverPoint, xLabel, yLabel, zLabel) { + 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'); + } + + it('should find closest point (case 1) and should', function() { + var pt = _hover(gd, 0.5, 0.5)[0]; + + expect(pt.index).toEqual([1, 0], 'have correct index'); + assertLabels(pt, 1, 1, 4); + }); + + it('should find closest point (case 2) and should', function() { + var pt = _hover(gd, 1.5, 0.5)[0]; + + expect(pt.index).toEqual([0, 0], 'have correct index'); + assertLabels(pt, 2, 0.2, 6); + }); + +});