Repository: holoviz/panel

Issue:
unexpected Interactivity of "layered" plotly objects in Tabs
My first one of these so apologies if it's missing info / already flagged.

#### Software info:
python: 3.6.9
Panel = 0.6.2
plotly = 4.2.1
bokeh = 1.3.4
jupyter notebook server = 6.0.0
browser: Google chrome (and same behavior in embedded html output)
OS: Windows 8.1

#### Description of expected behavior and the observed behavior
Below applies to within jupyter notebook, in browser window (.show()) and in html output (.save(embed = True))

__Expected behavior:__ plotly objects within panel tabs to have same interactivity as when not in tabs.

__observed behavior:__ Only plotly object in "bottom level" (last in list of tabs, "fig2" tab in example) retains full interactivity (pan, zoom, select, legend trace select etc.)). All other tab "levels" (tabs other than the last one in tab list, "fig1" tab inexample) retain only legend select interactivity. Interactions with the area bound by the axis (where a crosshair is seen) in "fig1" results in changes to the "fig2" plotly object.


#### Complete, minimal, self-contained example code that reproduces the issue

```
import plotly.graph_objs as go
import panel as pn
import numpy as np
pn.extension('plotly')

x = np.linspace(0,10,100)
y = np.sin(x)
y2 = np.cos(x)

data1 = [go.Scatter(x = x, y = y, name = 'scatter sin(x)', mode="markers+lines"),
       go.Scatter(x = x, y = y2, name = 'scatter cos(x)', mode="markers+lines")]

data2 = [go.Bar(x = x, y = y, name = 'bar sin(x)'),
       go.Bar(x = x, y = y2, name = 'bar cos(x)')]

fig1 = go.Figure(data = data1)
fig2 = go.Figure(data = data2)

pn.Column('## App with plotly objects in tabs:',
          'Loss of interactivity on "fig1" tab plotly object',
         pn.Tabs(('fig1', pn.Pane(fig1)),
                 ('fig2', pn.Pane(fig2)))
         )

```
#### Screenshots of issue

![Panel_Plotly_tabs_bug](https://user-images.githubusercontent.com/38754794/69160627-d1ae4580-0ae1-11ea-999d-5557841df206.png)




Discussion:
I am also having issues with the use of tabs causing a loss of plot interactivity except on the lowest level tab. This issue occurs for Plotly and hvplot objects, however it does not occur for Holoviews and Geoviews objects.

**Software Info:**
```
os x                      Catalina 
python                    3.7.5
notebook                  6.0.2 
pandas                    0.25.3
panel                     0.7.0
plotly                    4.3.0 
plotly_express            0.4.1 
holoviews                 1.12.6
geoviews                  1.6.5 
hvplot                    0.5.2 
```

**Description of the Problem:**

I have created the following panel called "example":

<img width="1194" alt="Screenshot 2019-12-17 at 12 18 35" src="https://user-images.githubusercontent.com/56728624/71001481-48386600-20d5-11ea-8ce4-9e654aa803b8.png">

This panel contains the following elements:

0. Tabs A,B,C.

     Each tab contains an identical GridSpec which consists of a:

1. Bar plot produced using hvplot (fig1).
2. Histogram created using hvplot. I used pn.interact to create the app "app_numeric" which displays the appropriate histogram when a numeric attribute is chosen from the dropdown.
3. UK choropleth created using Geoviews and Holoviews (map_app_df).

**Expected Behaviour**:
When using tabs, all elements 1. - 3. are fully interactive as they are when you plot just a single GridSpec.

**Observed Behaviour**:
Elements 1. and 3. are fully interactive as expected for **ALL 3 TABS**. If you hover over a graph component in either element then you are presented with the appropriate hover data. For element 3., the map is fully interactive and zoomable. Tabs to show or hide heatmap all work fine.

For Element 2. (the histogram), if the user hovers over any graph component then the appropriate hover data is displayed in all 3 tabs. However if the user tries to interact with element 2., eg by selecting an attribute from the dropdown to display the appropriate histogram, then this only works for tab C.

Furthermore, I noticed that if I choose an attribute from the dropdown, sometimes the selection doesn't seem to take and the plot doesn't update - this following error is then displayed in my Jupyter notebook:

`tornado.application - ERROR - Exception in callback functools.partial(<bound method IOLoop._discard_future_result of <tornado.platform.asyncio.AsyncIOMainLoop object at 0x10a97b890>>, <Future finished exception=AttributeError("'NoneType' object has no attribute 'values'")>)`

Please note that this error does not trigger when choosing an attribute from the dropdown in the panel app "app_numeric" directly, however it does intermittently occur when "app_numeric" is added to the GridSpec. 


I also created a second panel called "example2":

<img width="1196" alt="Screenshot 2019-12-17 at 12 19 04" src="https://user-images.githubusercontent.com/56728624/71004032-656f3380-20d9-11ea-8c19-b0166bf9d4c8.png">

This panel is identical to "example" except that Element 2. was created using Plotly here.
Result was identical to that of "example" except that:

- For each tab, when you hover over a graph component, no hover data is displayed for the histogram in Element 2. except for in the lowest level tab C.

- The tornado error seen above is never triggered when Element 2. was created using Plotly.

--------

**Code to Reproduce Issue:**
The following provides the code to generate the 2 Tabbed GridSpec panels "example" and "example2".

### Setup:

```python3
import pandas as pd
import numpy as np
import random
import copy
import feather
import plotly.graph_objects as go
import plotly.express as px
import panel as pn
import holoviews as hv
import geoviews as gv
import geoviews.feature as gf
import cartopy
import cartopy.feature as cf
from geoviews import opts
from cartopy import crs as ccrs
import hvplot.pandas
import colorcet as cc
from colorcet.plotting import swatch
gv.extension("bokeh")
```
### Create dataframe:
```python3
cols = {"name":["Jim","Alice","Bob","Julia","Fern","Bill","Jordan","Pip","Shelly","Mimi"], 
         "age":[19,26,37,45,56,71,20,36,37,55], 
         "age_band":["18-24","25-34","35-44","45-54","55-64","65-74","18-24","35-44","35-44","55-64"],
         "insurance_renew_month":[1,2,3,3,3,4,5,5,6,7],
         "postcode_prefix":["EH","M","G","EH","EH","M","G","EH","M","EH"],
         "postcode_order":[3,2,1,3,3,2,1,3,2,3],
         "local_authority_district":["S12000036","E08000003","S12000049","S12000036","S12000036","E08000003","S12000036","E08000003","S12000049","S12000036"],
         "blah1":[3,None,None,8,8,None,1,None,None,None],
         "blah2":[None,None,None,33,5,None,66,3,22,3],
         "blah3":["A",None,"A",None,"C",None,None,None,None,None],
         "blah4":[None,None,None,None,None,None,None,None,None,1]}
df = pd.DataFrame.from_dict(cols)
df
```
Out[2]: 
```python3
     name  age age_band  insurance_renew_month  ... blah1  blah2 blah3  blah4
0     Jim   19    18-24                      1  ...   3.0    NaN     A    NaN
1   Alice   26    25-34                      2  ...   NaN    NaN  None    NaN
2     Bob   37    35-44                      3  ...   NaN    NaN     A    NaN
3   Julia   45    45-54                      3  ...   8.0   33.0  None    NaN
4    Fern   56    55-64                      3  ...   8.0    5.0     C    NaN
5    Bill   71    65-74                      4  ...   NaN    NaN  None    NaN
6  Jordan   20    18-24                      5  ...   1.0   66.0  None    NaN
7     Pip   36    35-44                      5  ...   NaN    3.0  None    NaN
8  Shelly   37    35-44                      6  ...   NaN   22.0  None    NaN
9    Mimi   55    55-64                      7  ...   NaN    3.0  None    1.0

[10 rows x 11 columns]
```
### Prepare Data for Fig1 and Plot:
```python3
swatch("CET_L17")
normal_cmap = cc.CET_L17

df_count = df.count().sort_values(ascending=False)
df_count = df_count.to_frame().reset_index().rename(columns={"index":"attribute",0:"count"})
```
```python3
# Plot Fig1
fig1 =  df_count[::-1].hvplot.bar(x="attribute",y="count",color="count",cmap=normal_cmap,invert=True, width=500, height=300)
```

### Prepare and Plot UK Choropleth Map:
```python3
# Load Shapefile
shapefile = "/Users/maleko/Local_Authority_Districts_April_2019_Boundaries_UK_BUC/Local_Authority_Districts_April_2019_Boundaries_UK_BUC.shp"
gv.Shape.from_shapefile(shapefile, crs=ccrs.OSGB())
```


```python3
# Identify Attribute to Link Map & Data (It is "lad19cd"):
shapes = cartopy.io.shapereader.Reader(shapefile)
list(shapes.records())[0]
```

```python3
# Plot UK Choropleth:  
swatch("CET_R3")
new_cmap = cc.CET_R3
  
heatmap = gv.Shape.from_records(shapes.records(), df, on={"lad19cd":"local_authority_district"}, 
                                value="postcode_order",index="postcode_prefix", crs=ccrs.OSGB()).opts(tools=["hover"], 
                                cmap = new_cmap, colorbar=True, hover_color="white", hover_alpha=0, title="UK Postcode Heatmap", width=500, height=800)

non_heatmap = gv.Shape.from_records(shapes.records(), df, on={"lad19cd":"local_authority_district"}, 
                                    crs=ccrs.OSGB()).opts(title="UK Dealership Map", width=500, height=800)

wikimap = hv.Tiles('https://maps.wikimedia.org/osm-intl/{Z}/{X}/{Y}@2x.png', name="Wikimedia").opts(width=500, height=800)

pins = gv.Points([(-1.67514,54.96003,"Gateshead"),(-1.51591,54.52271,"Darlington"),(-2.28369,53.46123,"Manchester"),(-0.05944,51.64624,"Enfield"),(-3.73886,56.15171,"Tillicoultry")]).opts(tools=["hover"],color="#6a0dad",size = 15,hover_color="green") 

    
result1 = wikimap*heatmap*pins
result2 = wikimap*non_heatmap*pins                                                         

map_app_df = pn.Tabs(("Show Heatmap",result1),("Hide Heatmap",result2))
```

### A. Create Histogram Using hvplot:
```python3
num_atts = ["age","insurance_renew_month"]
num_atts

def num_plot_df(numeric="age"):
    if numeric =="age":
        fig2= df.hvplot.hist(y="age",by=["age_band"],
                  bins=[18,25,35,45,55,65,74],
                  xticks=[(21.5,"18-24"),(30,"25-34"),(40,"35-44"),(50,"45-54"),(60,"55-64"),(69.5,"65-74")],
                  color="teal",legend=False,
                  line_width=4,line_color="w", width=500, height=300)
        return fig2
    elif numeric =="insurance_renew_month":
        fig3 = df.hvplot.hist(y="insurance_renew_month",
                  bins=[1,2,3,4,5,6,7,8,9,10,11,12],
                  xticks=[(1.5,"JAN"),(2.5,"FEB"),(3.5,"MAR"),(4.5,"APR"),(5.5,"MAY"),(6.5,"JUN"),
                         (7.5,"JUL"),(8.5,"AUG"),(9.5,"SEP"),(10.5,"OCT"),(11.5,"NOV"),(12.5,"DEC")],
                  color="teal",legend=False,
                  line_width=4,line_color="w", width=500, height=300)
        return fig3
    
app_numeric = pn.interact(num_plot_df,numeric=num_atts)
```

### Create Gridspec For hvplot Version:
```python3
gspec = pn.GridSpec(sizing_mode="scale_both", max_height=1000)

gspec[0,0] = fig1
gspec[1,0] = app_numeric  
gspec[0:2,1] = map_app_df
```

### B. Create Histogram Using Plotly
```python3
def num_plot_df_plotly(numeric="age",nbins=12):
    if numeric in num_atts:
        numplot = px.histogram(df,x=numeric,nbins=nbins, width=500, height=300)
        numplot.update_traces(marker_line_color="rgb(255,255,255)",
            marker_line_width=4)
        return numplot
    
app_numeric2 = pn.interact(num_plot_df_plotly,numeric=num_atts,nbins=(1,20))
```

### Create Gridspec For Plotly Version:
```python3
gspec2 = pn.GridSpec(sizing_mode="scale_both", max_height=1000)

gspec2[0,0] = fig1
gspec2[1,0] = app_numeric2  
gspec2[0:2,1] = map_app_df
```

### Final tabbed Gridspec panel for the hvplot version:
```python3
example = pn.Tabs(("A",gspec),("B",gspec),("C",gspec))
example.show()
```
<img width="1194" alt="Screenshot 2019-12-17 at 12 18 35" src="https://user-images.githubusercontent.com/56728624/71000369-21793000-20d3-11ea-908a-ed34ca1fa6e4.png">

### Final tabbed Gridspec panel for the Plotly version:
```python3
example2 = pn.Tabs(("A",gspec2),("B",gspec2),("C",gspec2))
example2.show()
```
<img width="1196" alt="Screenshot 2019-12-17 at 12 19 04" src="https://user-images.githubusercontent.com/56728624/71000382-263de400-20d3-11ea-9a97-d2cae816e770.png">




@jonmmease Do you have any idea what might be happening here?
Hi, is there any update on this issue? Thanks
No sorry and if there was we'd post it here. It's not clear to me whether this is an issue with Panel, Bokeh or Plotly tbh so it's hard to figure out what is happening.
Hi.
I also ran into the same issue. One thing I noticed was that when the graphs on the tabs don't overlap each other the functionality works perfectly fine. But if they overlap, even though we are seeing the graph on top, the interactive functions are in fact effecting the bottom graph. 
This seems to suggest the issue is not with `plotly`, but with how `panel` "renders" the tabs. It seems like where the graphs overlap, the top graph is transparent to the interaction tools. I can't say anything more as I have no idea how both these libraries actually work. 

I'm attaching a self sufficient example which you can run to see what I am talking about, if its not clear from my explanation. Hope this helps to understand the problem.
I have shifted the bottom graph by specifying a margin. So the graphs now overlap only the sides, a little. Notice that interactivity works fine on the left side of the top-tab graph, but if you try hovering or zooming on the right side of the top graph nothing happens, but it's the bottom graph that gets zoomed in on. (To see what I am talking about you can try using the `Box Select` tool on the right side of the top tab and then switch to the bottom one to see.)

Red portion is where the graphs overlap. The blue portions are the parts of both the graphs that don't overlap. In the red region, the top tab interactivity doesn't work. 
![1](https://user-images.githubusercontent.com/25515417/80461030-49ff1980-8952-11ea-8b32-46db291590bb.jpg)
Code for the above example: 
```
import numpy as np
import param
import plotly.graph_objects as go
import plotly.figure_factory as ff
import panel as pn
pn.extension('plotly')

x = np.arange(1000)

class PlotTabs(param.Parameterized):
    log_x = param.Boolean(True, doc="X-axis log")
    log_y = param.Boolean(True, doc="Y-axis log")
    new_values = param.Action(lambda x: x.param.trigger('new_values'), label="New Sample")

    def __init__(self, **params):
        super(PlotTabs, self).__init__(**params)
        self.y1 = np.random.normal(loc=np.random.randint(0,5), size=1000)
        self.y2 = np.random.normal(loc=np.random.randint(0,5), size=1000)
        self.figure1 = go.Figure()
        self.figure2 = go.Figure()
        
    @param.depends('new_values', watch=True)
    def compute(self):
        self.y1 = np.random.normal(loc=np.random.randint(0,5), size=1000)
        self.y2 = np.random.normal(loc=np.random.randint(0,5), size=1000)
 
    @param.depends('new_values', 'log_x', 'log_y', watch=True)
    def view_timeseries(self):
        self.figure1 = go.Figure() # Resetting
        self.figure1.add_trace(go.Scatter(x=x, y=self.y1, mode='markers'))
        self.figure1.add_trace(go.Scatter(x=x, y=self.y2, mode='markers'))
        if self.log_x:
            self.figure1.update_layout(xaxis_type="log")
        if self.log_y:
            self.figure1.update_layout(yaxis_type='log')
        self.figure1.update_layout(autosize=True,legend_orientation='h', showlegend=True, \
                          margin=dict(l=200, r=20, t=20, b=20), width=500, height=300)
        return self.figure1

    @param.depends('new_values', watch=True)
    def view_histogram(self):
        self.figure2 = go.Figure()
        hist_data = [self.y1, self.y2]
        labels = ['y1', 'y2']
        self.figure2 = ff.create_distplot(hist_data, labels, bin_size=0.1, show_rug=False, show_curve=True)
        self.figure2.update_layout(autosize=True,legend_orientation='h', showlegend=True, \
                          margin=dict(l=20, r=20, t=20, b=20), width=300, height=300)
        return self.figure2
    

plot = PlotTabs(name="Example")
pn.Row(pn.Column(plot.param), 
       pn.Tabs(('Histogram',plot.view_histogram),
               ('Time Series', plot.view_timeseries),))
```

As a temporary workaround, I move all the content of the non-active tabs far up on the page by adjusting the margins. This way none of the invisible plotly figures overlap with the figures on the active tab.

This workaround works well, even for nested tabs. But I have to warn you that the response time for switching tabs becomes rather slow when you have a lot of tabs.

```
import numpy as np
import panel as pn
import plotly.graph_objects as go

pn.extension('plotly')


def fix_plots_in_tabs(plots):
    """Unfortunately, the plotly interactivity doesn't work when the plots overlap in panel,
    even if they are on different tabs. This is "fixed" by moving all the hidden plotly plots
    up, so they don't overlap with the one which is visible.

    issue thread: https://github.com/holoviz/panel/issues/804
    """

    for p in plots:
        if (isinstance(p, pn.layout.Tabs)):
            fix_plots_in_tabs(p)

    code = """
    for (let i = 0; i < source.tabs.length; i++) {
        var column = source.tabs[i.toString()].child
        if(source.active == i){
            column.margin = [0, 0, 0, 0]
        } else {
            column.margin = [-10000, 0, 0, 0]
        }
    }
    """

    plots.jscallback(active=code)

    for i in range(1, len(plots)):
        plots[i].margin = [-10000, 0, 0, 0]


plots = pn.Tabs(tabs_location="left")
plots_normal = pn.Tabs(tabs_location="left")
plots_inverse = pn.Tabs(tabs_location="left")
plots.append(("normal", plots_normal))
plots.append(("inverse", plots_inverse))

x = np.arange(10)
pn1 = go.Scatter(x=x, y=x**2, marker={"color": "red"})
pn2 = go.Scatter(x=x, y=x**3, marker={"color": "green"})
pi1 = go.Scatter(x=x**2, y=x, marker={"color": "blue"})
pi2 = go.Scatter(x=x**3, y=x, marker={"color": "black"})
plots_normal.append(("quadratic", pn1))
plots_normal.append(("third power", pn2))
plots_inverse.append(("quadratic", pi1))
plots_inverse.append(("third power", pi2))

fix_plots_in_tabs(plots)
plots.show()
```

![image](https://user-images.githubusercontent.com/53470294/84015587-5d9f9600-a97c-11ea-91ac-02e29c460acf.png)


Hi @philippjfr , 
       I also ran into the same issue. After a lot of searching and all, I ended up with analyzing the source code. Where I came to know that:
**Issue**
           When we click on any tab a new class **''tab-active"** is added to the corresponding **div**  in  _html file_, and **"visibility: hidden"** is removed from that tab's container's **div**, and rest all tabs will have class **"visibility: hidden"**.
According to documentation of **"visibility"**, _**Hidden** elements take up space on the page_.
So, this causes the issue illustrated by @Huggies23.
**Solution**
           In place of **"visibility: hidden"**, if we can replace it to **"display: none"**, we can solve the issue.
According to the documentation of **"display"**, _**None** elements will not take space on the page.

From my side I tried to add JavaScript to replace **"visibility: hidden"** to **"display: none"**, but was not successful because, the JavaScript was not called every time the tab changes. 

Hi, any progress on this issue?
Hi! Since it's quite an old bug and there is no much progress here, maybe it's worth to write a warning in documentation that it's better to avoid use Tabs together with interactive plots in Panel, what do you think? Otherwise Panel users have to debug this issue, then google this page and finally have to redesign their application.
> it's better to avoid use Tabs together with interactive plots in Panel, 

The above examples seem specifically related to Plotly, right?  I.e. it's not that interactive plots have trouble in tabs in Panel in general, but that specifically Plotly objects interact across tabs? If so such a general warning would not be appropriate; it's a very specific bug. It does sound like "display: none" is worth trying in Panel, though.
@jbednar agree, not general interactive plots, but it seems specifically Plotly objects together with Tabs create an issue. 
I'll see if I can come up with a fix very soon.
Hoping for fix to this issue soon. Plotly itself has tabs implemented in Dash.

Resolving patch:
diff --git a/panel/pane/plotly.py b/panel/pane/plotly.py
--- a/panel/pane/plotly.py
+++ b/panel/pane/plotly.py
@@ -5,12 +5,12 @@
 import numpy as np
 import param
 
-from bokeh.models import ColumnDataSource
+from bokeh.models import ColumnDataSource, CustomJS, Tabs
 from pyviz_comms import JupyterComm
 
 from .base import PaneBase
 from ..util import isdatetime, lazy_load
-from ..viewable import Layoutable
+from ..viewable import Layoutable, Viewable
 
 
 
@@ -206,6 +206,8 @@ def _get_model(self, doc, root=None, parent=None, comm=None):
             root = model
         self._link_props(model, self._linkable_params, doc, root, comm)
         self._models[root.ref['id']] = (model, parent)
+        if _patch_tabs_plotly not in Viewable._preprocessing_hooks:
+            Viewable._preprocessing_hooks.append(_patch_tabs_plotly)
         return model
 
     def _update(self, ref=None, model=None):
@@ -273,3 +275,42 @@ def _update(self, ref=None, model=None):
         # Check if we should trigger rendering
         if updates or update_sources:
             model._render_count += 1
+
+
+def _patch_tabs_plotly(viewable, root):
+    """
+    A preprocessing hook which ensures that any Plotly panes rendered
+    inside Tabs are only visible when the tab they are in is active.
+    This is a workaround for https://github.com/holoviz/panel/issues/804.
+    """
+    from ..models.plotly import PlotlyPlot
+
+    tabs_models = list(root.select({'type': Tabs}))
+    plotly_models = root.select({'type': PlotlyPlot})
+
+    for model in plotly_models:
+        parent_tabs = []
+        for tabs in tabs_models:
+            if tabs.select_one({'id': model.id}):
+                parent_tabs.append(tabs)
+        parent_tab = None
+        for tabs in parent_tabs:
+            if not any(tabs.select_one(pt) for pt in parent_tabs if pt is not tabs):
+                parent_tab = tabs
+                break
+        if parent_tab is None:
+            return
+        for i, tab in enumerate(parent_tab.tabs):
+            if tab.select_one({'id': model.id}):
+                break
+        updated = False
+        code = "model.visible = cb_obj.active == i;"
+        for cb in parent_tab.js_property_callbacks.get('change:active', []):
+            if cb.code == code and cb.args.get('model') is model:
+                cb.args['i'] = i
+                updated = True
+        if updated:
+            continue
+        callback = CustomJS(args={'model': model, 'i': i, 'margin': model.margin}, code=code)
+        parent_tab.js_on_change('active', callback)
+        model.visible = parent_tab.active == i


Test patch:
diff --git a/panel/tests/pane/test_plotly.py b/panel/tests/pane/test_plotly.py
--- a/panel/tests/pane/test_plotly.py
+++ b/panel/tests/pane/test_plotly.py
@@ -14,6 +14,7 @@
 import numpy as np
 
 from panel.models.plotly import PlotlyPlot
+from panel.layout import Tabs
 from panel.pane import PaneBase, Plotly
 
 
@@ -181,3 +182,32 @@ def test_plotly_autosize(document, comm):
     model.sizing_mode == 'fixed'
 
     pane._cleanup(model)
+
+
+@plotly_available
+def test_plotly_tabs(document, comm):
+    trace = go.Scatter(x=[0, 1], y=[2, 3])
+    
+    pane1 = Plotly(dict(data=[trace], layout={'autosize': True}))
+    pane2 = Plotly(dict(data=[trace], layout={'autosize': True}))
+
+    tabs = Tabs(pane1, pane2)
+
+    root = tabs.get_root(document, comm)
+
+    model1 = pane1._models[root.id][0]
+    model2 = pane2._models[root.id][0]
+
+    cb1, cb2 = root.js_property_callbacks['change:active']
+    if cb1.args['model'] is model2:
+        cb1, cb2 = cb2, cb1
+    assert model1.visible
+    assert cb1.args['model'] is model1
+    assert cb1.args['i'] == 0
+    assert not model2.visible
+    assert cb2.args['model'] is model2
+    assert cb2.args['i'] == 1
+
+    tabs.insert(0, 'Blah')
+    assert cb1.args['i'] == 1
+    assert cb2.args['i'] == 2