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/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 685e2a8af61..63ee8646baa 100644
--- a/src/traces/sunburst/plot.js
+++ b/src/traces/sunburst/plot.js
@@ -529,12 +529,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;
@@ -602,14 +602,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) {
@@ -617,18 +617,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 773bfbd2564..572d7a909ef 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() {
@@ -1384,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 995dd812df0..3a652e54228 100644
--- a/test/jasmine/tests/sunburst_test.js
+++ b/test/jasmine/tests/sunburst_test.js
@@ -1053,3 +1053,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);
+    });
+});