From 65c26783bd2b95e0bf2eae6ee331fcd6e1e5fdad Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= <etienne@plot.ly>
Date: Fri, 22 Mar 2019 12:56:45 -0400
Subject: [PATCH 1/2] fix visible true|false toggle for pie traces

- probably a side-effect from a splom-perf PR
---
 src/traces/pie/base_plot.js    |  3 +--
 test/jasmine/tests/pie_test.js | 20 ++++++++++++++++++++
 2 files changed, 21 insertions(+), 2 deletions(-)

diff --git a/src/traces/pie/base_plot.js b/src/traces/pie/base_plot.js
index cfc415501f0..af1d15543a1 100644
--- a/src/traces/pie/base_plot.js
+++ b/src/traces/pie/base_plot.js
@@ -16,8 +16,7 @@ exports.name = 'pie';
 exports.plot = function(gd) {
     var Pie = Registry.getModule('pie');
     var cdPie = getModuleCalcData(gd.calcdata, Pie)[0];
-
-    if(cdPie.length) Pie.plot(gd, cdPie);
+    Pie.plot(gd, cdPie);
 };
 
 exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) {
diff --git a/test/jasmine/tests/pie_test.js b/test/jasmine/tests/pie_test.js
index 773bfbd2564..76583ffba3d 100644
--- a/test/jasmine/tests/pie_test.js
+++ b/test/jasmine/tests/pie_test.js
@@ -818,6 +818,26 @@ describe('Pie traces', function() {
         .catch(failTest)
         .then(done);
     });
+
+    it('should be able to toggle visibility', function(done) {
+        var mock = Lib.extendDeep({}, require('@mocks/pie_title_multiple.json'));
+
+        function _assert(msg, exp) {
+            return function() {
+                var layer = d3.select(gd).select('.pielayer');
+                expect(layer.selectAll('.trace').size()).toBe(exp, msg);
+            };
+        }
+
+        Plotly.plot(gd, mock)
+        .then(_assert('base', 4))
+        .then(function() { return Plotly.restyle(gd, 'visible', false); })
+        .then(_assert('both visible:false', 0))
+        .then(function() { return Plotly.restyle(gd, 'visible', true); })
+        .then(_assert('back to visible:true', 4))
+        .catch(failTest)
+        .then(done);
+    });
 });
 
 describe('pie hovering', function() {

From e1084fa698a1dbbbf589b38d71d4c05b151589e9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= <etienne@plot.ly>
Date: Fri, 22 Mar 2019 12:58:23 -0400
Subject: [PATCH 2/2] fix #3618 edge for pie and sunburst

---
 src/traces/pie/plot.js              | 16 +++---
 src/traces/sunburst/plot.js         | 16 +++---
 test/jasmine/tests/pie_test.js      | 82 +++++++++++++++++++++++++++++
 test/jasmine/tests/sunburst_test.js | 67 +++++++++++++++++++++++
 4 files changed, 165 insertions(+), 16 deletions(-)

diff --git a/src/traces/pie/plot.js b/src/traces/pie/plot.js
index 47321269f17..8ca0f20f1ba 100644
--- a/src/traces/pie/plot.js
+++ b/src/traces/pie/plot.js
@@ -313,12 +313,12 @@ function attachFxHandlers(sliceTop, gd, cd) {
 
     // hover state vars
     // have we drawn a hover label, so it should be cleared later
-    var hasHoverLabel = false;
+    if(!('_hasHoverLabel' in trace)) trace._hasHoverLabel = false;
     // have we emitted a hover event, so later an unhover event should be emitted
     // note that click events do not depend on this - you can still get them
     // with hovermode: false or if you were earlier dragging, then clicked
     // in the same slice that you moused up in
-    var hasHoverEvent = false;
+    if(!('_hasHoverEvent' in trace)) trace._hasHoverEvent = false;
 
     sliceTop.on('mouseover', function(pt) {
         // in case fullLayout or fullData has changed without a replot
@@ -390,14 +390,14 @@ function attachFxHandlers(sliceTop, gd, cd) {
                 gd: gd
             });
 
-            hasHoverLabel = true;
+            trace._hasHoverLabel = true;
         }
 
+        trace._hasHoverEvent = true;
         gd.emit('plotly_hover', {
             points: [eventData(pt, trace2)],
             event: d3.event
         });
-        hasHoverEvent = true;
     });
 
     sliceTop.on('mouseout', function(evt) {
@@ -405,18 +405,18 @@ function attachFxHandlers(sliceTop, gd, cd) {
         var trace2 = gd._fullData[trace.index];
         var pt = d3.select(this).datum();
 
-        if(hasHoverEvent) {
+        if(trace._hasHoverEvent) {
             evt.originalEvent = d3.event;
             gd.emit('plotly_unhover', {
                 points: [eventData(pt, trace2)],
                 event: d3.event
             });
-            hasHoverEvent = false;
+            trace._hasHoverEvent = false;
         }
 
-        if(hasHoverLabel) {
+        if(trace._hasHoverLabel) {
             Fx.loneUnhover(fullLayout2._hoverlayer.node());
-            hasHoverLabel = false;
+            trace._hasHoverLabel = false;
         }
     });
 
diff --git a/src/traces/sunburst/plot.js b/src/traces/sunburst/plot.js
index 930f634ca2f..bdee7443036 100644
--- a/src/traces/sunburst/plot.js
+++ b/src/traces/sunburst/plot.js
@@ -530,12 +530,12 @@ function attachFxHandlers(sliceTop, gd, cd) {
 
     // hover state vars
     // have we drawn a hover label, so it should be cleared later
-    var hasHoverLabel = false;
+    if(!('_hasHoverLabel' in trace)) trace._hasHoverLabel = false;
     // have we emitted a hover event, so later an unhover event should be emitted
     // note that click events do not depend on this - you can still get them
     // with hovermode: false or if you were earlier dragging, then clicked
     // in the same slice that you moused up in
-    var hasHoverEvent = false;
+    if(!('_hasHoverEvent' in trace)) trace._hasHoverEvent = false;
 
     sliceTop.on('mouseover', function(pt) {
         var fullLayoutNow = gd._fullLayout;
@@ -603,14 +603,14 @@ function attachFxHandlers(sliceTop, gd, cd) {
                 gd: gd
             });
 
-            hasHoverLabel = true;
+            trace._hasHoverLabel = true;
         }
 
+        trace._hasHoverEvent = true;
         gd.emit('plotly_hover', {
             points: [makeEventData(pt, traceNow)],
             event: d3.event
         });
-        hasHoverEvent = true;
     });
 
     sliceTop.on('mouseout', function(evt) {
@@ -618,18 +618,18 @@ function attachFxHandlers(sliceTop, gd, cd) {
         var traceNow = gd._fullData[trace.index];
         var pt = d3.select(this).datum();
 
-        if(hasHoverEvent) {
+        if(trace._hasHoverEvent) {
             evt.originalEvent = d3.event;
             gd.emit('plotly_unhover', {
                 points: [makeEventData(pt, traceNow)],
                 event: d3.event
             });
-            hasHoverEvent = false;
+            trace._hasHoverEvent = false;
         }
 
-        if(hasHoverLabel) {
+        if(trace._hasHoverLabel) {
             Fx.loneUnhover(fullLayoutNow._hoverlayer.node());
-            hasHoverLabel = false;
+            trace._hasHoverLabel = false;
         }
     });
 
diff --git a/test/jasmine/tests/pie_test.js b/test/jasmine/tests/pie_test.js
index 76583ffba3d..572d7a909ef 100644
--- a/test/jasmine/tests/pie_test.js
+++ b/test/jasmine/tests/pie_test.js
@@ -1404,3 +1404,85 @@ describe('pie relayout', function() {
         .then(done);
     });
 });
+
+describe('Test pie interactions edge cases:', function() {
+    var gd;
+
+    beforeEach(function() { gd = createGraphDiv(); });
+
+    afterEach(destroyGraphDiv);
+
+    function _mouseEvent(type, v) {
+        return function() {
+            var el = d3.select(gd).select('.slice:nth-child(' + v + ')').node();
+            mouseEvent(type, 0, 0, {element: el});
+        };
+    }
+
+    function hover(v) {
+        return _mouseEvent('mouseover', v);
+    }
+
+    function unhover(v) {
+        return _mouseEvent('mouseout', v);
+    }
+
+    it('should keep tracking hover labels and hover events after *calc* edits', function(done) {
+        var mock = Lib.extendFlat({}, require('@mocks/pie_simple.json'));
+        var hoverCnt = 0;
+        var unhoverCnt = 0;
+
+        // see https://github.com/plotly/plotly.js/issues/3618
+
+        function _assert(msg, exp) {
+            expect(hoverCnt).toBe(exp.hoverCnt, msg + ' - hover cnt');
+            expect(unhoverCnt).toBe(exp.unhoverCnt, msg + ' - unhover cnt');
+
+            var label = d3.select(gd).select('g.hovertext');
+            expect(label.size()).toBe(exp.hoverLabel, msg + ' - hover label cnt');
+
+            hoverCnt = 0;
+            unhoverCnt = 0;
+        }
+
+        Plotly.plot(gd, mock)
+        .then(function() {
+            gd.on('plotly_hover', function() {
+                hoverCnt++;
+                // N.B. trigger a 'calc' edit
+                Plotly.restyle(gd, 'textinfo', 'percent');
+            });
+            gd.on('plotly_unhover', function() {
+                unhoverCnt++;
+                // N.B. trigger a 'calc' edit
+                Plotly.restyle(gd, 'textinfo', null);
+            });
+        })
+        .then(hover(1))
+        .then(function() {
+            _assert('after hovering on first sector', {
+                hoverCnt: 1,
+                unhoverCnt: 0,
+                hoverLabel: 1
+            });
+        })
+        .then(unhover(1))
+        .then(function() {
+            _assert('after un-hovering from first sector', {
+                hoverCnt: 0,
+                unhoverCnt: 1,
+                hoverLabel: 0
+            });
+        })
+        .then(hover(2))
+        .then(function() {
+            _assert('after hovering onto second sector', {
+                hoverCnt: 1,
+                unhoverCnt: 0,
+                hoverLabel: 1
+            });
+        })
+        .catch(failTest)
+        .then(done);
+    });
+});
diff --git a/test/jasmine/tests/sunburst_test.js b/test/jasmine/tests/sunburst_test.js
index e7c1664b418..f1190a994cb 100644
--- a/test/jasmine/tests/sunburst_test.js
+++ b/test/jasmine/tests/sunburst_test.js
@@ -1031,3 +1031,70 @@ describe('Test sunburst tweening:', function() {
         .then(done);
     });
 });
+
+describe('Test sunburst interactions edge cases', function() {
+    var gd;
+
+    beforeEach(function() { gd = createGraphDiv(); });
+
+    afterEach(destroyGraphDiv);
+
+    it('should keep tracking hover labels and hover events after *calc* edits', function(done) {
+        var mock = Lib.extendFlat({}, require('@mocks/sunburst_first.json'));
+        var hoverCnt = 0;
+        var unhoverCnt = 0;
+
+        // see https://github.com/plotly/plotly.js/issues/3618
+
+        function _assert(msg, exp) {
+            expect(hoverCnt).toBe(exp.hoverCnt, msg + ' - hover cnt');
+            expect(unhoverCnt).toBe(exp.unhoverCnt, msg + ' - unhover cnt');
+
+            var label = d3.select(gd).select('g.hovertext');
+            expect(label.size()).toBe(exp.hoverLabel, msg + ' - hover label cnt');
+
+            hoverCnt = 0;
+            unhoverCnt = 0;
+        }
+
+        Plotly.plot(gd, mock)
+        .then(function() {
+            gd.on('plotly_hover', function() {
+                hoverCnt++;
+                // N.B. trigger a 'plot' edit
+                Plotly.restyle(gd, 'textinfo', 'none');
+            });
+            gd.on('plotly_unhover', function() {
+                unhoverCnt++;
+                // N.B. trigger a 'plot' edit
+                Plotly.restyle(gd, 'textinfo', null);
+            });
+        })
+        .then(hover(gd, 1))
+        .then(function() {
+            _assert('after hovering on first sector', {
+                hoverCnt: 1,
+                unhoverCnt: 0,
+                hoverLabel: 1
+            });
+        })
+        .then(unhover(gd, 1))
+        .then(function() {
+            _assert('after un-hovering from first sector', {
+                hoverCnt: 0,
+                unhoverCnt: 1,
+                hoverLabel: 0
+            });
+        })
+        .then(hover(gd, 2))
+        .then(function() {
+            _assert('after hovering onto second sector', {
+                hoverCnt: 1,
+                unhoverCnt: 0,
+                hoverLabel: 1
+            });
+        })
+        .catch(failTest)
+        .then(done);
+    });
+});