From acce9bf5515d6443085080bd224d32152094230a Mon Sep 17 00:00:00 2001
From: Juernjakob Dugge <juernjakob@gmail.com>
Date: Sun, 15 Oct 2017 21:13:11 +0200
Subject: [PATCH 1/8] Add hoverlabel.zformat attribute

---
 src/components/fx/attributes.js          | 11 +++++++
 src/components/fx/calc.js                |  1 +
 src/components/fx/hover.js               |  9 +++++-
 src/components/fx/hoverlabel_defaults.js |  1 +
 src/components/fx/layout_attributes.js   | 11 +++++++
 test/image/mocks/heatmap_hoverlabel.json | 39 ++++++++++++++++++++++++
 6 files changed, 71 insertions(+), 1 deletion(-)
 create mode 100644 test/image/mocks/heatmap_hoverlabel.json

diff --git a/src/components/fx/attributes.js b/src/components/fx/attributes.js
index 1dd31102375..76b6491c66c 100644
--- a/src/components/fx/attributes.js
+++ b/src/components/fx/attributes.js
@@ -50,6 +50,17 @@ module.exports = {
                 '`namelength - 3` characters and add an ellipsis.'
             ].join(' ')
         },
+        zformat: {
+            valType: 'string',
+            dflt: '',
+            role: 'style',
+            editType: 'none',
+            description: [
+                'Sets the hover text formatting rule using d3 formatting mini-languages',
+                'which are very similar to those in Python. See:',
+                'https://github.com/d3/d3-format/blob/master/README.md#locale_format'
+            ].join(' ')
+        },
         editType: 'calc'
     }
 };
diff --git a/src/components/fx/calc.js b/src/components/fx/calc.js
index 677dc620778..1dd4c2b3d08 100644
--- a/src/components/fx/calc.js
+++ b/src/components/fx/calc.js
@@ -42,6 +42,7 @@ module.exports = function calc(gd) {
         fillFn(trace.hoverlabel.font.color, cd, 'htc');
         fillFn(trace.hoverlabel.font.family, cd, 'htf');
         fillFn(trace.hoverlabel.namelength, cd, 'hnl');
+        fillFn(trace.hoverlabel.zformat, cd, 'hzf');
     }
 };
 
diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js
index bc5866198bd..9eac7c8d00e 100644
--- a/src/components/fx/hover.js
+++ b/src/components/fx/hover.js
@@ -1054,6 +1054,7 @@ function cleanPoint(d, hovermode) {
     fill('fontSize', 'hts', 'hoverlabel.font.size');
     fill('fontColor', 'htc', 'hoverlabel.font.color');
     fill('nameLength', 'hnl', 'hoverlabel.namelength');
+    fill('zformat', 'hzf', 'hoverlabel.zformat');
 
     d.posref = hovermode === 'y' ? (d.x0 + d.x1) / 2 : (d.y0 + d.y1) / 2;
 
@@ -1095,7 +1096,13 @@ function cleanPoint(d, hovermode) {
         d.yVal = d.ya.c2d(d.yLabelVal);
     }
 
-    if(d.zLabelVal !== undefined) d.zLabel = String(d.zLabelVal);
+    if(d.zLabelVal !== undefined) {
+        if(d.zformat !== undefined) {
+            d.zLabel = d3.format(d.zformat)(d.zLabelVal).replace(/-/g, constants.MINUS_SIGN);
+        } else {
+            d.zLabel = String(d.zLabelVal);
+        }
+    }
 
     // for box means and error bars, add the range to the label
     if(!isNaN(d.xerr) && !(d.xa.type === 'log' && d.xerr <= 0)) {
diff --git a/src/components/fx/hoverlabel_defaults.js b/src/components/fx/hoverlabel_defaults.js
index 0b3573fe92d..205e97bb874 100644
--- a/src/components/fx/hoverlabel_defaults.js
+++ b/src/components/fx/hoverlabel_defaults.js
@@ -16,5 +16,6 @@ module.exports = function handleHoverLabelDefaults(contIn, contOut, coerce, opts
     coerce('hoverlabel.bgcolor', opts.bgcolor);
     coerce('hoverlabel.bordercolor', opts.bordercolor);
     coerce('hoverlabel.namelength', opts.namelength);
+    coerce('hoverlabel.zformat', opts.zformat);
     Lib.coerceFont(coerce, 'hoverlabel.font', opts.font);
 };
diff --git a/src/components/fx/layout_attributes.js b/src/components/fx/layout_attributes.js
index 25c6503a0ce..c37957a9b24 100644
--- a/src/components/fx/layout_attributes.js
+++ b/src/components/fx/layout_attributes.js
@@ -72,6 +72,17 @@ module.exports = {
                 '`namelength - 3` characters and add an ellipsis.'
             ].join(' ')
         },
+        zformat: {
+            valType: 'string',
+            dflt: '',
+            role: 'style',
+            editType: 'none',
+            description: [
+                'Sets the hover text formatting rule using d3 formatting mini-languages',
+                'which are very similar to those in Python. See:',
+                'https://github.com/d3/d3-format/blob/master/README.md#locale_format'
+            ].join(' ')
+        },
         editType: 'none'
     }
 };
diff --git a/test/image/mocks/heatmap_hoverlabel.json b/test/image/mocks/heatmap_hoverlabel.json
new file mode 100644
index 00000000000..953f2a19be1
--- /dev/null
+++ b/test/image/mocks/heatmap_hoverlabel.json
@@ -0,0 +1,39 @@
+{
+  "data": [
+    {
+      "z": [
+        [
+          0.123456789,
+          10.123456789,
+          20.123456789
+        ]
+      ],
+      "type": "heatmap"
+    },
+    {
+      "x": [0,1,2],
+      "y": [2, 2, 2],
+      "z": [
+          1.123456789,
+          2.123456789,
+          3.123456789
+        ],
+      "type": "heatmap",
+      "hoverlabel": {
+        "zformat": "undefined"
+      },
+      "zmin": 0,
+      "zmax": 20,
+      "showscale": false
+    }
+  ],
+  "layout": {
+    "title": "XXX",
+    "xaxis": {
+      "hoverformat": ".1f"
+    },
+    "hoverlabel": {
+      "zformat": ".2f"
+    }
+  }
+}

From b918176fe8a83811269c28ffa343081656826adb Mon Sep 17 00:00:00 2001
From: Juernjakob Dugge <juernjakob@gmail.com>
Date: Wed, 18 Oct 2017 22:31:49 +0200
Subject: [PATCH 2/8] Move property out of hoverlabel, use Axis.tickText for
 formatting, write tests

---
 src/components/fx/attributes.js          | 11 -----
 src/components/fx/calc.js                |  1 -
 src/components/fx/hover.js               |  9 +----
 src/components/fx/hoverlabel_defaults.js |  1 -
 src/traces/heatmap/attributes.js         | 11 +++++
 src/traces/heatmap/defaults.js           |  3 ++
 src/traces/heatmap/hover.js              | 16 ++++++++
 test/image/mocks/heatmap_hoverlabel.json | 39 ------------------
 test/jasmine/tests/heatmap_test.js       | 51 ++++++++++++++++++++----
 test/jasmine/tests/hover_label_test.js   | 37 +++++++++++++++++
 10 files changed, 112 insertions(+), 67 deletions(-)
 delete mode 100644 test/image/mocks/heatmap_hoverlabel.json

diff --git a/src/components/fx/attributes.js b/src/components/fx/attributes.js
index 76b6491c66c..1dd31102375 100644
--- a/src/components/fx/attributes.js
+++ b/src/components/fx/attributes.js
@@ -50,17 +50,6 @@ module.exports = {
                 '`namelength - 3` characters and add an ellipsis.'
             ].join(' ')
         },
-        zformat: {
-            valType: 'string',
-            dflt: '',
-            role: 'style',
-            editType: 'none',
-            description: [
-                'Sets the hover text formatting rule using d3 formatting mini-languages',
-                'which are very similar to those in Python. See:',
-                'https://github.com/d3/d3-format/blob/master/README.md#locale_format'
-            ].join(' ')
-        },
         editType: 'calc'
     }
 };
diff --git a/src/components/fx/calc.js b/src/components/fx/calc.js
index 1dd4c2b3d08..677dc620778 100644
--- a/src/components/fx/calc.js
+++ b/src/components/fx/calc.js
@@ -42,7 +42,6 @@ module.exports = function calc(gd) {
         fillFn(trace.hoverlabel.font.color, cd, 'htc');
         fillFn(trace.hoverlabel.font.family, cd, 'htf');
         fillFn(trace.hoverlabel.namelength, cd, 'hnl');
-        fillFn(trace.hoverlabel.zformat, cd, 'hzf');
     }
 };
 
diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js
index 9eac7c8d00e..8c25908e286 100644
--- a/src/components/fx/hover.js
+++ b/src/components/fx/hover.js
@@ -1054,7 +1054,6 @@ function cleanPoint(d, hovermode) {
     fill('fontSize', 'hts', 'hoverlabel.font.size');
     fill('fontColor', 'htc', 'hoverlabel.font.color');
     fill('nameLength', 'hnl', 'hoverlabel.namelength');
-    fill('zformat', 'hzf', 'hoverlabel.zformat');
 
     d.posref = hovermode === 'y' ? (d.x0 + d.x1) / 2 : (d.y0 + d.y1) / 2;
 
@@ -1096,12 +1095,8 @@ function cleanPoint(d, hovermode) {
         d.yVal = d.ya.c2d(d.yLabelVal);
     }
 
-    if(d.zLabelVal !== undefined) {
-        if(d.zformat !== undefined) {
-            d.zLabel = d3.format(d.zformat)(d.zLabelVal).replace(/-/g, constants.MINUS_SIGN);
-        } else {
-            d.zLabel = String(d.zLabelVal);
-        }
+    if(d.zLabelVal !== undefined && d.zLabel === undefined) { // Traces like heatmaps generate the zLabel in their hoverPoints function
+        d.zLabel = String(d.zLabelVal);
     }
 
     // for box means and error bars, add the range to the label
diff --git a/src/components/fx/hoverlabel_defaults.js b/src/components/fx/hoverlabel_defaults.js
index 205e97bb874..0b3573fe92d 100644
--- a/src/components/fx/hoverlabel_defaults.js
+++ b/src/components/fx/hoverlabel_defaults.js
@@ -16,6 +16,5 @@ module.exports = function handleHoverLabelDefaults(contIn, contOut, coerce, opts
     coerce('hoverlabel.bgcolor', opts.bgcolor);
     coerce('hoverlabel.bordercolor', opts.bordercolor);
     coerce('hoverlabel.namelength', opts.namelength);
-    coerce('hoverlabel.zformat', opts.zformat);
     Lib.coerceFont(coerce, 'hoverlabel.font', opts.font);
 };
diff --git a/src/traces/heatmap/attributes.js b/src/traces/heatmap/attributes.js
index 372197a5f7b..e967f17860a 100644
--- a/src/traces/heatmap/attributes.js
+++ b/src/traces/heatmap/attributes.js
@@ -100,6 +100,17 @@ module.exports = extendFlat({}, {
         editType: 'plot',
         description: 'Sets the vertical gap (in pixels) between bricks.'
     },
+    zhoverformat: {
+        valType: 'string',
+        dflt: '',
+        role: 'style',
+        editType: 'none',
+        description: [
+            'Sets the hover text formatting rule using d3 formatting mini-languages',
+            'which are very similar to those in Python. See:',
+            'https://github.com/d3/d3-format/blob/master/README.md#locale_format'
+        ].join(' ')
+    },
 },
     colorscaleAttrs,
     { autocolorscale: extendFlat({}, colorscaleAttrs.autocolorscale, {dflt: false}) },
diff --git a/src/traces/heatmap/defaults.js b/src/traces/heatmap/defaults.js
index 3dbbaa0f380..b80c76d6e2b 100644
--- a/src/traces/heatmap/defaults.js
+++ b/src/traces/heatmap/defaults.js
@@ -40,4 +40,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
     coerce('connectgaps', hasColumns(traceOut) && (traceOut.zsmooth !== false));
 
     colorscaleDefaults(traceIn, traceOut, layout, coerce, {prefix: '', cLetter: 'z'});
+
+    coerce('zhoverformat');
+    traceOut._separators = layout.separators; // Needed for formatting of hoverlabel if format is not explicitly specified
 };
diff --git a/src/traces/heatmap/hover.js b/src/traces/heatmap/hover.js
index e3a870f3cb7..2993f2262b1 100644
--- a/src/traces/heatmap/hover.js
+++ b/src/traces/heatmap/hover.js
@@ -11,6 +11,7 @@
 
 var Fx = require('../../components/fx');
 var Lib = require('../../lib');
+var Axes = require('../../plots/cartesian/axes');
 
 var MAXDIST = Fx.constants.MAXDIST;
 
@@ -26,6 +27,10 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, contour)
         y = cd0.y,
         z = cd0.z,
         zmask = cd0.zmask,
+        zmin = trace.zmin,
+        zmax = trace.zmax,
+        zhoverformat = trace.zhoverformat,
+        _separators = trace._separators,
         x2 = x,
         y2 = y,
         xl,
@@ -99,6 +104,16 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, contour)
         text = cd0.text[ny][nx];
     }
 
+    var zLabel;
+    var dummyAx = { // dummy axis for formatting the z value
+        type: 'linear',
+        range: [zmin, zmax],
+        hoverformat: zhoverformat,
+        _separators: _separators
+    };
+    var zLabelObj = Axes.tickText(dummyAx, zVal, 'hover');
+    zLabel = zLabelObj.text;
+
     return [Lib.extendFlat(pointData, {
         index: [ny, nx],
         // never let a 2D override 1D type as closest point
@@ -110,6 +125,7 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, contour)
         xLabelVal: xl,
         yLabelVal: yl,
         zLabelVal: zVal,
+        zLabel: zLabel,
         text: text
     })];
 };
diff --git a/test/image/mocks/heatmap_hoverlabel.json b/test/image/mocks/heatmap_hoverlabel.json
deleted file mode 100644
index 953f2a19be1..00000000000
--- a/test/image/mocks/heatmap_hoverlabel.json
+++ /dev/null
@@ -1,39 +0,0 @@
-{
-  "data": [
-    {
-      "z": [
-        [
-          0.123456789,
-          10.123456789,
-          20.123456789
-        ]
-      ],
-      "type": "heatmap"
-    },
-    {
-      "x": [0,1,2],
-      "y": [2, 2, 2],
-      "z": [
-          1.123456789,
-          2.123456789,
-          3.123456789
-        ],
-      "type": "heatmap",
-      "hoverlabel": {
-        "zformat": "undefined"
-      },
-      "zmin": 0,
-      "zmax": 20,
-      "showscale": false
-    }
-  ],
-  "layout": {
-    "title": "XXX",
-    "xaxis": {
-      "hoverformat": ".1f"
-    },
-    "hoverlabel": {
-      "zformat": ".2f"
-    }
-  }
-}
diff --git a/test/jasmine/tests/heatmap_test.js b/test/jasmine/tests/heatmap_test.js
index 81e7737c736..b22155665ba 100644
--- a/test/jasmine/tests/heatmap_test.js
+++ b/test/jasmine/tests/heatmap_test.js
@@ -616,10 +616,11 @@ describe('heatmap hover', function() {
         return hoverData;
     }
 
-    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');
+    function assertLabels(hoverPoint, xLabelVal, yLabelVal, zLabelVal, zLabel, text) {
+        expect(hoverPoint.xLabelVal).toEqual(xLabelVal, 'have correct x label value');
+        expect(hoverPoint.yLabelVal).toEqual(yLabelVal, 'have correct y label value');
+        expect(hoverPoint.zLabelVal).toEqual(zLabelVal, 'have correct z label value');
+        expect(hoverPoint.zLabel).toEqual(zLabel, 'have correct z label');
         expect(hoverPoint.text).toEqual(text, 'have correct text label');
     }
 
@@ -640,14 +641,14 @@ describe('heatmap hover', function() {
             var pt = _hover(gd, 0.5, 0.5)[0];
 
             expect(pt.index).toEqual([1, 0], 'have correct index');
-            assertLabels(pt, 1, 1, 4);
+            assertLabels(pt, 1, 1, 4, '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);
+            assertLabels(pt, 2, 0.2, 6, '6');
         });
     });
 
@@ -673,13 +674,47 @@ describe('heatmap hover', function() {
             var pt = _hover(gd, 0.5, 0.5)[0];
 
             expect(pt.index).toEqual([0, 0], 'have correct index');
-            assertLabels(pt, 1, 1, 10, 'a');
+            assertLabels(pt, 1, 1, 10, '10', 'a');
 
             Plotly.relayout(gd, 'xaxis.range', [1, 2]).then(function() {
                 var pt2 = _hover(gd, 1.5, 0.5)[0];
 
                 expect(pt2.index).toEqual([0, 1], 'have correct index');
-                assertLabels(pt2, 2, 1, 4, 'b');
+                assertLabels(pt2, 2, 1, 4, '4', 'b');
+            })
+            .then(done);
+        });
+
+    });
+
+    describe('for hovering with specific number format', function() {
+
+        beforeAll(function(done) {
+            gd = createGraphDiv();
+
+            Plotly.plot(gd, [{
+                type: 'heatmap',
+                x: [1, 2, 3],
+                y: [1, 1, 1],
+                z: [0.123456789, 2.9999, 4],
+                zhoverformat: '.2f'
+            }])
+            .then(done);
+        });
+
+        afterAll(destroyGraphDiv);
+
+        it('should find closest point and should', function(done) {
+            var pt = _hover(gd, 0.5, 0.5)[0];
+
+            expect(pt.index).toEqual([0, 0], 'have correct index');
+            assertLabels(pt, 1, 1, 0.123456789, '0.12');
+
+            Plotly.relayout(gd, 'xaxis.range', [1, 2]).then(function() {
+                var pt2 = _hover(gd, 1.5, 0.5)[0];
+
+                expect(pt2.index).toEqual([0, 1], 'have correct index');
+                assertLabels(pt2, 2, 1, 2.9999, '3.00');
             })
             .then(done);
         });
diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js
index b6be796bdbe..b8fa0ad2693 100644
--- a/test/jasmine/tests/hover_label_test.js
+++ b/test/jasmine/tests/hover_label_test.js
@@ -499,6 +499,43 @@ describe('hover info', function() {
             .catch(fail)
             .then(done);
         });
+
+        it('should display correct label content with specified format', function(done) {
+            var gd = createGraphDiv();
+
+            Plotly.plot(gd, [{
+                type: 'heatmap',
+                y: [0, 1],
+                z: [[1.11111, 2.2222, 3.33333], [4.44444, 5.55555, 6.66666]],
+                name: 'one',
+                zhoverformat: '.2f'
+            }, {
+                type: 'heatmap',
+                y: [2, 3],
+                z: [[1, 2, 3], [2, 2, 1]],
+                name: 'two'
+            }], {
+                width: 500,
+                height: 400,
+                margin: {l: 0, t: 0, r: 0, b: 0}
+            })
+            .then(function() {
+                _hover(gd, 250, 100);
+                assertHoverLabelContent({
+                    nums: 'x: 1\ny: 3\nz: 2',
+                    name: 'two'
+                });
+            })
+            .then(function() {
+                _hover(gd, 250, 300);
+                assertHoverLabelContent({
+                    nums: 'x: 1\ny: 1\nz: 5.56',
+                    name: 'one'
+                });
+            })
+            .catch(fail)
+            .then(done);
+        });
     });
 
     describe('hoverformat', function() {

From 7a27542d08bdb2f1cfb2a323cb3490ff3df7fc23 Mon Sep 17 00:00:00 2001
From: Juernjakob Dugge <juernjakob@gmail.com>
Date: Wed, 18 Oct 2017 22:34:04 +0200
Subject: [PATCH 3/8] Remove zformat from hoverlabels layout properties

---
 src/components/fx/layout_attributes.js | 11 -----------
 1 file changed, 11 deletions(-)

diff --git a/src/components/fx/layout_attributes.js b/src/components/fx/layout_attributes.js
index c37957a9b24..25c6503a0ce 100644
--- a/src/components/fx/layout_attributes.js
+++ b/src/components/fx/layout_attributes.js
@@ -72,17 +72,6 @@ module.exports = {
                 '`namelength - 3` characters and add an ellipsis.'
             ].join(' ')
         },
-        zformat: {
-            valType: 'string',
-            dflt: '',
-            role: 'style',
-            editType: 'none',
-            description: [
-                'Sets the hover text formatting rule using d3 formatting mini-languages',
-                'which are very similar to those in Python. See:',
-                'https://github.com/d3/d3-format/blob/master/README.md#locale_format'
-            ].join(' ')
-        },
         editType: 'none'
     }
 };

From 919feea33d7d3668df714511f1eabdeed638a423 Mon Sep 17 00:00:00 2001
From: Juernjakob Dugge <juernjakob@gmail.com>
Date: Thu, 19 Oct 2017 22:39:56 +0200
Subject: [PATCH 4/8] Add zhoverformat to contour plots

---
 src/components/fx/hover.js         |  3 +-
 src/traces/contour/attributes.js   |  1 +
 src/traces/contour/defaults.js     |  4 +++
 src/traces/heatmap/hover.js        |  8 ++---
 test/jasmine/tests/heatmap_test.js | 51 +++++-------------------------
 5 files changed, 19 insertions(+), 48 deletions(-)

diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js
index 8c25908e286..5b91220a73e 100644
--- a/src/components/fx/hover.js
+++ b/src/components/fx/hover.js
@@ -1095,7 +1095,8 @@ function cleanPoint(d, hovermode) {
         d.yVal = d.ya.c2d(d.yLabelVal);
     }
 
-    if(d.zLabelVal !== undefined && d.zLabel === undefined) { // Traces like heatmaps generate the zLabel in their hoverPoints function
+    // Traces like heatmaps generate the zLabel in their hoverPoints function
+    if(d.zLabelVal !== undefined && d.zLabel === undefined) {
         d.zLabel = String(d.zLabelVal);
     }
 
diff --git a/src/traces/contour/attributes.js b/src/traces/contour/attributes.js
index 5414afe9f48..8db880b2317 100644
--- a/src/traces/contour/attributes.js
+++ b/src/traces/contour/attributes.js
@@ -30,6 +30,7 @@ module.exports = extendFlat({
     transpose: heatmapAttrs.transpose,
     xtype: heatmapAttrs.xtype,
     ytype: heatmapAttrs.ytype,
+    zhoverformat: heatmapAttrs.zhoverformat,
 
     connectgaps: heatmapAttrs.connectgaps,
 
diff --git a/src/traces/contour/defaults.js b/src/traces/contour/defaults.js
index a616f818045..1e30d6b17c5 100644
--- a/src/traces/contour/defaults.js
+++ b/src/traces/contour/defaults.js
@@ -34,4 +34,8 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
 
     handleContoursDefaults(traceIn, traceOut, coerce);
     handleStyleDefaults(traceIn, traceOut, coerce, layout);
+
+    coerce('zhoverformat');
+    // Needed for formatting of hoverlabel if format is not explicitly specified
+    traceOut._separators = layout.separators;
 };
diff --git a/src/traces/heatmap/hover.js b/src/traces/heatmap/hover.js
index 2993f2262b1..310f92c0b84 100644
--- a/src/traces/heatmap/hover.js
+++ b/src/traces/heatmap/hover.js
@@ -27,8 +27,7 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, contour)
         y = cd0.y,
         z = cd0.z,
         zmask = cd0.zmask,
-        zmin = trace.zmin,
-        zmax = trace.zmax,
+        range = [trace.zmin, trace.zmax],
         zhoverformat = trace.zhoverformat,
         _separators = trace._separators,
         x2 = x,
@@ -105,9 +104,10 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, contour)
     }
 
     var zLabel;
-    var dummyAx = { // dummy axis for formatting the z value
+    // dummy axis for formatting the z value
+    var dummyAx = {
         type: 'linear',
-        range: [zmin, zmax],
+        range: range,
         hoverformat: zhoverformat,
         _separators: _separators
     };
diff --git a/test/jasmine/tests/heatmap_test.js b/test/jasmine/tests/heatmap_test.js
index b22155665ba..81e7737c736 100644
--- a/test/jasmine/tests/heatmap_test.js
+++ b/test/jasmine/tests/heatmap_test.js
@@ -616,11 +616,10 @@ describe('heatmap hover', function() {
         return hoverData;
     }
 
-    function assertLabels(hoverPoint, xLabelVal, yLabelVal, zLabelVal, zLabel, text) {
-        expect(hoverPoint.xLabelVal).toEqual(xLabelVal, 'have correct x label value');
-        expect(hoverPoint.yLabelVal).toEqual(yLabelVal, 'have correct y label value');
-        expect(hoverPoint.zLabelVal).toEqual(zLabelVal, 'have correct z label value');
-        expect(hoverPoint.zLabel).toEqual(zLabel, 'have correct z label');
+    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');
     }
 
@@ -641,14 +640,14 @@ describe('heatmap hover', function() {
             var pt = _hover(gd, 0.5, 0.5)[0];
 
             expect(pt.index).toEqual([1, 0], 'have correct index');
-            assertLabels(pt, 1, 1, 4, '4');
+            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, '6');
+            assertLabels(pt, 2, 0.2, 6);
         });
     });
 
@@ -674,47 +673,13 @@ describe('heatmap hover', function() {
             var pt = _hover(gd, 0.5, 0.5)[0];
 
             expect(pt.index).toEqual([0, 0], 'have correct index');
-            assertLabels(pt, 1, 1, 10, '10', 'a');
+            assertLabels(pt, 1, 1, 10, 'a');
 
             Plotly.relayout(gd, 'xaxis.range', [1, 2]).then(function() {
                 var pt2 = _hover(gd, 1.5, 0.5)[0];
 
                 expect(pt2.index).toEqual([0, 1], 'have correct index');
-                assertLabels(pt2, 2, 1, 4, '4', 'b');
-            })
-            .then(done);
-        });
-
-    });
-
-    describe('for hovering with specific number format', function() {
-
-        beforeAll(function(done) {
-            gd = createGraphDiv();
-
-            Plotly.plot(gd, [{
-                type: 'heatmap',
-                x: [1, 2, 3],
-                y: [1, 1, 1],
-                z: [0.123456789, 2.9999, 4],
-                zhoverformat: '.2f'
-            }])
-            .then(done);
-        });
-
-        afterAll(destroyGraphDiv);
-
-        it('should find closest point and should', function(done) {
-            var pt = _hover(gd, 0.5, 0.5)[0];
-
-            expect(pt.index).toEqual([0, 0], 'have correct index');
-            assertLabels(pt, 1, 1, 0.123456789, '0.12');
-
-            Plotly.relayout(gd, 'xaxis.range', [1, 2]).then(function() {
-                var pt2 = _hover(gd, 1.5, 0.5)[0];
-
-                expect(pt2.index).toEqual([0, 1], 'have correct index');
-                assertLabels(pt2, 2, 1, 2.9999, '3.00');
+                assertLabels(pt2, 2, 1, 4, 'b');
             })
             .then(done);
         });

From 5e20a483e14ff6230cc53965242011e30827ea62 Mon Sep 17 00:00:00 2001
From: Juernjakob Dugge <juernjakob@gmail.com>
Date: Sun, 15 Oct 2017 21:13:11 +0200
Subject: [PATCH 5/8] Add zhoverformat to heatmap and contour traces

---
 src/components/fx/hover.js             |  5 +++-
 src/traces/contour/attributes.js       |  1 +
 src/traces/contour/defaults.js         |  4 +++
 src/traces/heatmap/attributes.js       | 11 ++++++++
 src/traces/heatmap/defaults.js         |  3 +++
 src/traces/heatmap/hover.js            | 16 +++++++++++
 test/jasmine/tests/hover_label_test.js | 37 ++++++++++++++++++++++++++
 7 files changed, 76 insertions(+), 1 deletion(-)

diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js
index bc5866198bd..5b91220a73e 100644
--- a/src/components/fx/hover.js
+++ b/src/components/fx/hover.js
@@ -1095,7 +1095,10 @@ function cleanPoint(d, hovermode) {
         d.yVal = d.ya.c2d(d.yLabelVal);
     }
 
-    if(d.zLabelVal !== undefined) d.zLabel = String(d.zLabelVal);
+    // Traces like heatmaps generate the zLabel in their hoverPoints function
+    if(d.zLabelVal !== undefined && d.zLabel === undefined) {
+        d.zLabel = String(d.zLabelVal);
+    }
 
     // for box means and error bars, add the range to the label
     if(!isNaN(d.xerr) && !(d.xa.type === 'log' && d.xerr <= 0)) {
diff --git a/src/traces/contour/attributes.js b/src/traces/contour/attributes.js
index 5414afe9f48..8db880b2317 100644
--- a/src/traces/contour/attributes.js
+++ b/src/traces/contour/attributes.js
@@ -30,6 +30,7 @@ module.exports = extendFlat({
     transpose: heatmapAttrs.transpose,
     xtype: heatmapAttrs.xtype,
     ytype: heatmapAttrs.ytype,
+    zhoverformat: heatmapAttrs.zhoverformat,
 
     connectgaps: heatmapAttrs.connectgaps,
 
diff --git a/src/traces/contour/defaults.js b/src/traces/contour/defaults.js
index a616f818045..1e30d6b17c5 100644
--- a/src/traces/contour/defaults.js
+++ b/src/traces/contour/defaults.js
@@ -34,4 +34,8 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
 
     handleContoursDefaults(traceIn, traceOut, coerce);
     handleStyleDefaults(traceIn, traceOut, coerce, layout);
+
+    coerce('zhoverformat');
+    // Needed for formatting of hoverlabel if format is not explicitly specified
+    traceOut._separators = layout.separators;
 };
diff --git a/src/traces/heatmap/attributes.js b/src/traces/heatmap/attributes.js
index 372197a5f7b..e967f17860a 100644
--- a/src/traces/heatmap/attributes.js
+++ b/src/traces/heatmap/attributes.js
@@ -100,6 +100,17 @@ module.exports = extendFlat({}, {
         editType: 'plot',
         description: 'Sets the vertical gap (in pixels) between bricks.'
     },
+    zhoverformat: {
+        valType: 'string',
+        dflt: '',
+        role: 'style',
+        editType: 'none',
+        description: [
+            'Sets the hover text formatting rule using d3 formatting mini-languages',
+            'which are very similar to those in Python. See:',
+            'https://github.com/d3/d3-format/blob/master/README.md#locale_format'
+        ].join(' ')
+    },
 },
     colorscaleAttrs,
     { autocolorscale: extendFlat({}, colorscaleAttrs.autocolorscale, {dflt: false}) },
diff --git a/src/traces/heatmap/defaults.js b/src/traces/heatmap/defaults.js
index 3dbbaa0f380..b80c76d6e2b 100644
--- a/src/traces/heatmap/defaults.js
+++ b/src/traces/heatmap/defaults.js
@@ -40,4 +40,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
     coerce('connectgaps', hasColumns(traceOut) && (traceOut.zsmooth !== false));
 
     colorscaleDefaults(traceIn, traceOut, layout, coerce, {prefix: '', cLetter: 'z'});
+
+    coerce('zhoverformat');
+    traceOut._separators = layout.separators; // Needed for formatting of hoverlabel if format is not explicitly specified
 };
diff --git a/src/traces/heatmap/hover.js b/src/traces/heatmap/hover.js
index e3a870f3cb7..310f92c0b84 100644
--- a/src/traces/heatmap/hover.js
+++ b/src/traces/heatmap/hover.js
@@ -11,6 +11,7 @@
 
 var Fx = require('../../components/fx');
 var Lib = require('../../lib');
+var Axes = require('../../plots/cartesian/axes');
 
 var MAXDIST = Fx.constants.MAXDIST;
 
@@ -26,6 +27,9 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, contour)
         y = cd0.y,
         z = cd0.z,
         zmask = cd0.zmask,
+        range = [trace.zmin, trace.zmax],
+        zhoverformat = trace.zhoverformat,
+        _separators = trace._separators,
         x2 = x,
         y2 = y,
         xl,
@@ -99,6 +103,17 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, contour)
         text = cd0.text[ny][nx];
     }
 
+    var zLabel;
+    // dummy axis for formatting the z value
+    var dummyAx = {
+        type: 'linear',
+        range: range,
+        hoverformat: zhoverformat,
+        _separators: _separators
+    };
+    var zLabelObj = Axes.tickText(dummyAx, zVal, 'hover');
+    zLabel = zLabelObj.text;
+
     return [Lib.extendFlat(pointData, {
         index: [ny, nx],
         // never let a 2D override 1D type as closest point
@@ -110,6 +125,7 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, contour)
         xLabelVal: xl,
         yLabelVal: yl,
         zLabelVal: zVal,
+        zLabel: zLabel,
         text: text
     })];
 };
diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js
index b6be796bdbe..b8fa0ad2693 100644
--- a/test/jasmine/tests/hover_label_test.js
+++ b/test/jasmine/tests/hover_label_test.js
@@ -499,6 +499,43 @@ describe('hover info', function() {
             .catch(fail)
             .then(done);
         });
+
+        it('should display correct label content with specified format', function(done) {
+            var gd = createGraphDiv();
+
+            Plotly.plot(gd, [{
+                type: 'heatmap',
+                y: [0, 1],
+                z: [[1.11111, 2.2222, 3.33333], [4.44444, 5.55555, 6.66666]],
+                name: 'one',
+                zhoverformat: '.2f'
+            }, {
+                type: 'heatmap',
+                y: [2, 3],
+                z: [[1, 2, 3], [2, 2, 1]],
+                name: 'two'
+            }], {
+                width: 500,
+                height: 400,
+                margin: {l: 0, t: 0, r: 0, b: 0}
+            })
+            .then(function() {
+                _hover(gd, 250, 100);
+                assertHoverLabelContent({
+                    nums: 'x: 1\ny: 3\nz: 2',
+                    name: 'two'
+                });
+            })
+            .then(function() {
+                _hover(gd, 250, 300);
+                assertHoverLabelContent({
+                    nums: 'x: 1\ny: 1\nz: 5.56',
+                    name: 'one'
+                });
+            })
+            .catch(fail)
+            .then(done);
+        });
     });
 
     describe('hoverformat', function() {

From 86277f21bcdd6867f6f55bbbb5af81cf8477e678 Mon Sep 17 00:00:00 2001
From: Juernjakob Dugge <juernjakob@gmail.com>
Date: Fri, 20 Oct 2017 06:52:02 +0200
Subject: [PATCH 6/8] Fix formatting

---
 src/components/fx/hover.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js
index b4ea861cd40..4888f353cf2 100644
--- a/src/components/fx/hover.js
+++ b/src/components/fx/hover.js
@@ -1073,7 +1073,7 @@ function cleanPoint(d, hovermode) {
         d.yLabel = ('yLabel' in d) ? d.yLabel : Axes.hoverLabelText(d.ya, d.yLabelVal);
         d.yVal = d.ya.c2d(d.yLabelVal);
     }
-  
+
     // Traces like heatmaps generate the zLabel in their hoverPoints function
     if(d.zLabelVal !== undefined && d.zLabel === undefined) {
         d.zLabel = String(d.zLabelVal);

From 1760d660b7815c5adfed7b7edd26ea26dac2a77e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= <etienne@plot.ly>
Date: Fri, 20 Oct 2017 10:13:56 -0400
Subject: [PATCH 7/8] fixup choropleth select test

- PR https://github.com/plotly/plotly.js/pull/2099 got merged
  on a branch behind https://github.com/plotly/plotly.js/pull/2081
  which caused the test to fail on master.
---
 test/jasmine/tests/select_test.js | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js
index 9915cb9583f..72f29593e3f 100644
--- a/test/jasmine/tests/select_test.js
+++ b/test/jasmine/tests/select_test.js
@@ -717,10 +717,11 @@ describe('Test select box and lasso per trace:', function() {
         addInvisible(fig, false);
 
         // add a trace with no locations which will then make trace invisible, lacking DOM elements
-        fig.data.push(Lib.extendDeep({}, fig.data[0]));
-        fig.data[1].text = [];
-        fig.data[1].locations = [];
-        fig.data[1].z = [];
+        var emptyChoroplethTrace = Lib.extendDeep({}, fig.data[0]);
+        emptyChoroplethTrace.text = [];
+        emptyChoroplethTrace.locations = [];
+        emptyChoroplethTrace.z = [];
+        fig.data.push(emptyChoroplethTrace);
 
         Plotly.plot(gd, fig)
         .then(function() {

From 48509f36647cf6d46f4e3f219e706b7b265bb91f Mon Sep 17 00:00:00 2001
From: Juernjakob Dugge <juernjakob@gmail.com>
Date: Sun, 15 Oct 2017 21:13:11 +0200
Subject: [PATCH 8/8] Add zhoverformat to heatmap and contour traces

---
 src/components/fx/hover.js             |  6 ++++-
 src/traces/contour/attributes.js       |  1 +
 src/traces/contour/defaults.js         |  4 +++
 src/traces/heatmap/attributes.js       | 11 ++++++++
 src/traces/heatmap/defaults.js         |  3 +++
 src/traces/heatmap/hover.js            | 16 +++++++++++
 test/jasmine/tests/hover_label_test.js | 37 ++++++++++++++++++++++++++
 7 files changed, 77 insertions(+), 1 deletion(-)

diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js
index cc366193eb3..4888f353cf2 100644
--- a/src/components/fx/hover.js
+++ b/src/components/fx/hover.js
@@ -1073,7 +1073,11 @@ function cleanPoint(d, hovermode) {
         d.yLabel = ('yLabel' in d) ? d.yLabel : Axes.hoverLabelText(d.ya, d.yLabelVal);
         d.yVal = d.ya.c2d(d.yLabelVal);
     }
-    if(d.zLabelVal !== undefined) d.zLabel = String(d.zLabelVal);
+
+    // Traces like heatmaps generate the zLabel in their hoverPoints function
+    if(d.zLabelVal !== undefined && d.zLabel === undefined) {
+        d.zLabel = String(d.zLabelVal);
+    }
 
     // for box means and error bars, add the range to the label
     if(!isNaN(d.xerr) && !(d.xa.type === 'log' && d.xerr <= 0)) {
diff --git a/src/traces/contour/attributes.js b/src/traces/contour/attributes.js
index 5414afe9f48..8db880b2317 100644
--- a/src/traces/contour/attributes.js
+++ b/src/traces/contour/attributes.js
@@ -30,6 +30,7 @@ module.exports = extendFlat({
     transpose: heatmapAttrs.transpose,
     xtype: heatmapAttrs.xtype,
     ytype: heatmapAttrs.ytype,
+    zhoverformat: heatmapAttrs.zhoverformat,
 
     connectgaps: heatmapAttrs.connectgaps,
 
diff --git a/src/traces/contour/defaults.js b/src/traces/contour/defaults.js
index a616f818045..1e30d6b17c5 100644
--- a/src/traces/contour/defaults.js
+++ b/src/traces/contour/defaults.js
@@ -34,4 +34,8 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
 
     handleContoursDefaults(traceIn, traceOut, coerce);
     handleStyleDefaults(traceIn, traceOut, coerce, layout);
+
+    coerce('zhoverformat');
+    // Needed for formatting of hoverlabel if format is not explicitly specified
+    traceOut._separators = layout.separators;
 };
diff --git a/src/traces/heatmap/attributes.js b/src/traces/heatmap/attributes.js
index 372197a5f7b..e967f17860a 100644
--- a/src/traces/heatmap/attributes.js
+++ b/src/traces/heatmap/attributes.js
@@ -100,6 +100,17 @@ module.exports = extendFlat({}, {
         editType: 'plot',
         description: 'Sets the vertical gap (in pixels) between bricks.'
     },
+    zhoverformat: {
+        valType: 'string',
+        dflt: '',
+        role: 'style',
+        editType: 'none',
+        description: [
+            'Sets the hover text formatting rule using d3 formatting mini-languages',
+            'which are very similar to those in Python. See:',
+            'https://github.com/d3/d3-format/blob/master/README.md#locale_format'
+        ].join(' ')
+    },
 },
     colorscaleAttrs,
     { autocolorscale: extendFlat({}, colorscaleAttrs.autocolorscale, {dflt: false}) },
diff --git a/src/traces/heatmap/defaults.js b/src/traces/heatmap/defaults.js
index 3dbbaa0f380..b80c76d6e2b 100644
--- a/src/traces/heatmap/defaults.js
+++ b/src/traces/heatmap/defaults.js
@@ -40,4 +40,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
     coerce('connectgaps', hasColumns(traceOut) && (traceOut.zsmooth !== false));
 
     colorscaleDefaults(traceIn, traceOut, layout, coerce, {prefix: '', cLetter: 'z'});
+
+    coerce('zhoverformat');
+    traceOut._separators = layout.separators; // Needed for formatting of hoverlabel if format is not explicitly specified
 };
diff --git a/src/traces/heatmap/hover.js b/src/traces/heatmap/hover.js
index e3a870f3cb7..310f92c0b84 100644
--- a/src/traces/heatmap/hover.js
+++ b/src/traces/heatmap/hover.js
@@ -11,6 +11,7 @@
 
 var Fx = require('../../components/fx');
 var Lib = require('../../lib');
+var Axes = require('../../plots/cartesian/axes');
 
 var MAXDIST = Fx.constants.MAXDIST;
 
@@ -26,6 +27,9 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, contour)
         y = cd0.y,
         z = cd0.z,
         zmask = cd0.zmask,
+        range = [trace.zmin, trace.zmax],
+        zhoverformat = trace.zhoverformat,
+        _separators = trace._separators,
         x2 = x,
         y2 = y,
         xl,
@@ -99,6 +103,17 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, contour)
         text = cd0.text[ny][nx];
     }
 
+    var zLabel;
+    // dummy axis for formatting the z value
+    var dummyAx = {
+        type: 'linear',
+        range: range,
+        hoverformat: zhoverformat,
+        _separators: _separators
+    };
+    var zLabelObj = Axes.tickText(dummyAx, zVal, 'hover');
+    zLabel = zLabelObj.text;
+
     return [Lib.extendFlat(pointData, {
         index: [ny, nx],
         // never let a 2D override 1D type as closest point
@@ -110,6 +125,7 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, contour)
         xLabelVal: xl,
         yLabelVal: yl,
         zLabelVal: zVal,
+        zLabel: zLabel,
         text: text
     })];
 };
diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js
index b6be796bdbe..b8fa0ad2693 100644
--- a/test/jasmine/tests/hover_label_test.js
+++ b/test/jasmine/tests/hover_label_test.js
@@ -499,6 +499,43 @@ describe('hover info', function() {
             .catch(fail)
             .then(done);
         });
+
+        it('should display correct label content with specified format', function(done) {
+            var gd = createGraphDiv();
+
+            Plotly.plot(gd, [{
+                type: 'heatmap',
+                y: [0, 1],
+                z: [[1.11111, 2.2222, 3.33333], [4.44444, 5.55555, 6.66666]],
+                name: 'one',
+                zhoverformat: '.2f'
+            }, {
+                type: 'heatmap',
+                y: [2, 3],
+                z: [[1, 2, 3], [2, 2, 1]],
+                name: 'two'
+            }], {
+                width: 500,
+                height: 400,
+                margin: {l: 0, t: 0, r: 0, b: 0}
+            })
+            .then(function() {
+                _hover(gd, 250, 100);
+                assertHoverLabelContent({
+                    nums: 'x: 1\ny: 3\nz: 2',
+                    name: 'two'
+                });
+            })
+            .then(function() {
+                _hover(gd, 250, 300);
+                assertHoverLabelContent({
+                    nums: 'x: 1\ny: 1\nz: 5.56',
+                    name: 'one'
+                });
+            })
+            .catch(fail)
+            .then(done);
+        });
     });
 
     describe('hoverformat', function() {