From 609104d17e8509348d9ff1c47b7e79fd837ae401 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= <etienne@plot.ly>
Date: Tue, 26 Mar 2019 12:44:17 -0400
Subject: [PATCH 1/2] fix hovertemplate fallback when hovering on scatter fills

---
 src/components/fx/hover.js             | 12 ++++++----
 src/traces/scatter/hover.js            |  2 +-
 test/jasmine/tests/hover_label_test.js | 32 ++++++++++++++++++++++++++
 3 files changed, 41 insertions(+), 5 deletions(-)

diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js
index 0cb5df93b86..76ada0f47b0 100644
--- a/src/components/fx/hover.js
+++ b/src/components/fx/hover.js
@@ -672,11 +672,15 @@ function _hover(gd, evt, subplot, noHoverEvent) {
         var pt = hoverData[itemnum];
         var eventData = helpers.makeEventData(pt, pt.trace, pt.cd);
 
-        var ht = false;
-        if(pt.cd[pt.index] && pt.cd[pt.index].ht) ht = pt.cd[pt.index].ht;
-        hoverData[itemnum].hovertemplate = ht || pt.trace.hovertemplate || false;
-        hoverData[itemnum].eventData = [eventData];
+        if(pt.hovertemplate !== false) {
+            var ht = false;
+            if(pt.cd[pt.index] && pt.cd[pt.index].ht) {
+                ht = pt.cd[pt.index].ht;
+            }
+            pt.hovertemplate = ht || pt.trace.hovertemplate || false;
+        }
 
+        pt.eventData = [eventData];
         newhoverdata.push(eventData);
     }
 
diff --git a/src/traces/scatter/hover.js b/src/traces/scatter/hover.js
index d82509e1c98..c1c9410eb5e 100644
--- a/src/traces/scatter/hover.js
+++ b/src/traces/scatter/hover.js
@@ -179,7 +179,7 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
                 y0: yAvg,
                 y1: yAvg,
                 color: color,
-                hovertemplate: '%{name}'
+                hovertemplate: false
             });
 
             delete pointData.index;
diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js
index 89792ed50ae..973a458e2c0 100644
--- a/test/jasmine/tests/hover_label_test.js
+++ b/test/jasmine/tests/hover_label_test.js
@@ -1938,6 +1938,38 @@ describe('hover info', function() {
         .catch(failTest)
         .then(done);
     });
+
+    it('should fallback to regular hover content when hoveron does not support hovertemplate', function(done) {
+        var gd = createGraphDiv();
+        var fig = Lib.extendDeep({}, require('@mocks/scatter_fill_self_next.json'));
+
+        fig.data.forEach(function(trace) {
+            trace.hoveron = 'points+fills';
+            trace.hovertemplate = '%{x} | %{y}';
+        });
+
+        fig.layout.hovermode = 'closest';
+        fig.layout.showlegend = false;
+        fig.layout.margin = {t: 0, b: 0, l: 0, r: 0};
+
+        Plotly.plot(gd, fig)
+        .then(function() { _hoverNatural(gd, 180, 200); })
+        .then(function() {
+            assertHoverLabelContent({
+                nums: 'trace 1',
+                name: ''
+            }, 'hovering on a fill');
+        })
+        .then(function() { _hoverNatural(gd, 50, 95); })
+        .then(function() {
+            assertHoverLabelContent({
+                nums: '0 | 5',
+                name: 'trace 1'
+            }, 'hovering on a pt');
+        })
+        .catch(failTest)
+        .then(done);
+    });
 });
 
 describe('hover info on stacked subplots', function() {

From ea34d6c02c91587663d1d35179abfb661000fa05 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= <etienne@plot.ly>
Date: Tue, 26 Mar 2019 12:44:44 -0400
Subject: [PATCH 2/2] add hovertemplate to box/violin points

---
 src/traces/box/attributes.js      |  6 ++++
 src/traces/box/defaults.js        |  5 +++-
 src/traces/box/hover.js           |  7 ++++-
 src/traces/violin/attributes.js   |  1 +
 src/traces/violin/hover.js        |  3 ++
 test/jasmine/tests/box_test.js    | 47 +++++++++++++++++++++++++++++++
 test/jasmine/tests/violin_test.js | 46 ++++++++++++++++++++++++++++++
 7 files changed, 113 insertions(+), 2 deletions(-)

diff --git a/src/traces/box/attributes.js b/src/traces/box/attributes.js
index 3a8a1f3c4c8..66476cb1f26 100644
--- a/src/traces/box/attributes.js
+++ b/src/traces/box/attributes.js
@@ -11,6 +11,7 @@
 var scatterAttrs = require('../scatter/attributes');
 var barAttrs = require('../bar/attributes');
 var colorAttrs = require('../../components/color/attributes');
+var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes');
 var extendFlat = require('../../lib/extend').extendFlat;
 
 var scatterMarkerAttrs = scatterAttrs.marker;
@@ -76,6 +77,11 @@ module.exports = {
     hovertext: extendFlat({}, scatterAttrs.hovertext, {
         description: 'Same as `text`.'
     }),
+    hovertemplate: hovertemplateAttrs({
+        description: [
+            'N.B. This only has an effect when hovering on points.'
+        ].join(' ')
+    }),
     whiskerwidth: {
         valType: 'number',
         min: 0,
diff --git a/src/traces/box/defaults.js b/src/traces/box/defaults.js
index e3d61a37b69..3bba43eb33d 100644
--- a/src/traces/box/defaults.js
+++ b/src/traces/box/defaults.js
@@ -105,7 +105,10 @@ function handlePointsDefaults(traceIn, traceOut, coerce, opts) {
         delete traceOut.marker;
     }
 
-    coerce('hoveron');
+    var hoveron = coerce('hoveron');
+    if(hoveron === 'all' || hoveron.indexOf('points') !== -1) {
+        coerce('hovertemplate');
+    }
 
     Lib.coerceSelectionMarkerOpacity(traceOut, coerce);
 }
diff --git a/src/traces/box/hover.js b/src/traces/box/hover.js
index e2cc11969a6..c97d040db43 100644
--- a/src/traces/box/hover.js
+++ b/src/traces/box/hover.js
@@ -176,11 +176,15 @@ function hoverOnBoxes(pointData, xval, yval, hovermode) {
         if(attr === 'mean' && ('sd' in di) && trace.boxmean === 'sd') {
             pointData2[vLetter + 'err'] = di.sd;
         }
+
         // only keep name and spikes on the first item (median)
         pointData.name = '';
         pointData.spikeDistance = undefined;
         pointData[spikePosAttr] = undefined;
 
+        // no hovertemplate support yet
+        pointData2.hovertemplate = false;
+
         closeBoxData.push(pointData2);
     }
 
@@ -242,7 +246,8 @@ function hoverOnPoints(pointData, xval, yval) {
         x1: xc + rad,
         y0: yc - rad,
         y1: yc + rad,
-        spikeDistance: pointData.distance
+        spikeDistance: pointData.distance,
+        hovertemplate: trace.hovertemplate
     });
 
     var pa;
diff --git a/src/traces/violin/attributes.js b/src/traces/violin/attributes.js
index 2b7e08a713f..af5f76d44f1 100644
--- a/src/traces/violin/attributes.js
+++ b/src/traces/violin/attributes.js
@@ -147,6 +147,7 @@ module.exports = {
     marker: boxAttrs.marker,
     text: boxAttrs.text,
     hovertext: boxAttrs.hovertext,
+    hovertemplate: boxAttrs.hovertemplate,
 
     box: {
         visible: {
diff --git a/src/traces/violin/hover.js b/src/traces/violin/hover.js
index cb70d91a63e..184e41744c7 100644
--- a/src/traces/violin/hover.js
+++ b/src/traces/violin/hover.js
@@ -71,6 +71,9 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, hoverLay
                 closeBoxData[0].spikeDistance = undefined;
                 closeBoxData[0][spikePosAttr] = undefined;
 
+                // no hovertemplate support yet
+                kdePointData.hovertemplate = false;
+
                 closeData.push(kdePointData);
 
                 violinLineAttrs = {stroke: pointData.color};
diff --git a/test/jasmine/tests/box_test.js b/test/jasmine/tests/box_test.js
index 527b540c806..86b2bc6e552 100644
--- a/test/jasmine/tests/box_test.js
+++ b/test/jasmine/tests/box_test.js
@@ -152,6 +152,40 @@ describe('Test boxes supplyDefaults', function() {
         expect(traceOut.text).toBeDefined();
     });
 
+    describe('should not coerce hovertemplate when *hoveron* does not contains *points* flag', function() {
+        var ht = '--- %{y}';
+
+        it('- case hoveron:points', function() {
+            traceIn = {
+                y: [1, 1, 2],
+                hoveron: 'points',
+                hovertemplate: ht
+            };
+            supplyDefaults(traceIn, traceOut, defaultColor, {});
+            expect(traceOut.hovertemplate).toBe(ht);
+        });
+
+        it('- case hoveron:points+boxes', function() {
+            traceIn = {
+                y: [1, 1, 2],
+                hoveron: 'points+boxes',
+                hovertemplate: ht
+            };
+            supplyDefaults(traceIn, traceOut, defaultColor, {});
+            expect(traceOut.hovertemplate).toBe(ht);
+        });
+
+        it('- case hoveron:boxes', function() {
+            traceIn = {
+                y: [1, 1, 2],
+                hoveron: 'boxes',
+                hovertemplate: ht
+            };
+            supplyDefaults(traceIn, traceOut, defaultColor, {});
+            expect(traceOut.hovertemplate).toBe(undefined);
+        });
+    });
+
     it('should not include alignementgroup/offsetgroup when boxmode is not *group*', function() {
         var gd = {
             data: [{type: 'box', y: [1], alignmentgroup: 'a', offsetgroup: '1'}],
@@ -391,6 +425,19 @@ describe('Test box hover:', function() {
         pos: [202, 335],
         nums: '(2, 13.1)',
         name: ''
+    }, {
+        desc: 'with hovertemplate for points',
+        patch: function(fig) {
+            fig.data.forEach(function(trace) {
+                trace.boxpoints = 'all';
+                trace.hoveron = 'points';
+                trace.hovertemplate = '%{y}<extra>pt #%{pointNumber}</extra>';
+            });
+            fig.layout.hovermode = 'closest';
+            return fig;
+        },
+        nums: '0.6',
+        name: 'pt #0'
     }].forEach(function(specs) {
         it('should generate correct hover labels ' + specs.desc, function(done) {
             run(specs).catch(failTest).then(done);
diff --git a/test/jasmine/tests/violin_test.js b/test/jasmine/tests/violin_test.js
index 231e26a19f9..80bfa26d8a5 100644
--- a/test/jasmine/tests/violin_test.js
+++ b/test/jasmine/tests/violin_test.js
@@ -160,6 +160,38 @@ describe('Test violin defaults', function() {
         expect(traceOut.scalegroup).toBe('');
     });
 
+    it('should not coerce hovertemplate when *hoveron* does not contains *points* flag', function() {
+        var ht = '--- %{y} ---';
+
+        _supply({
+            y: [1, 2, 1],
+            hoveron: 'points',
+            hovertemplate: ht
+        });
+        expect(traceOut.hovertemplate).toBe(ht, 'hoveron:points');
+
+        _supply({
+            y: [1, 2, 1],
+            hoveron: 'kde',
+            hovertemplate: ht
+        });
+        expect(traceOut.hovertemplate).toBe(undefined, 'hoveron:kde');
+
+        _supply({
+            y: [1, 2, 1],
+            hoveron: 'all',
+            hovertemplate: ht
+        });
+        expect(traceOut.hovertemplate).toBe(ht, 'hoveron:all');
+
+        _supply({
+            y: [1, 2, 1],
+            hoveron: 'violins+points',
+            hovertemplate: ht
+        });
+        expect(traceOut.hovertemplate).toBe(ht, 'hoveron:violins+points');
+    });
+
     it('should not include alignementgroup/offsetgroup when violinmode is not *group*', function() {
         var gd = {
             data: [{type: 'violin', y: [1], alignmentgroup: 'a', offsetgroup: '1'}],
@@ -591,6 +623,20 @@ describe('Test violin hover:', function() {
         pos: [417, 309],
         nums: '(14, 2)',
         name: ''
+    }, {
+        desc: 'with hovertemplate for points',
+        patch: function(fig) {
+            fig.data.forEach(function(trace) {
+                trace.points = 'all';
+                trace.hoveron = 'points';
+                trace.hovertemplate = 'Sample pt %{pointNumber}: %{y:.3f}<extra></extra>';
+            });
+            fig.layout.hovermode = 'closest';
+            return fig;
+        },
+        pos: [220, 200],
+        nums: 'Sample pt 3: 0.900',
+        name: ''
     }]
     .forEach(function(specs) {
         it('should generate correct hover labels ' + specs.desc, function(done) {