Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

unexpected Interactivity of "layered" plotly objects in Tabs #804

Closed
Huggies23 opened this issue Nov 19, 2019 · 14 comments · Fixed by #2418
Closed

unexpected Interactivity of "layered" plotly objects in Tabs #804

Huggies23 opened this issue Nov 19, 2019 · 14 comments · Fixed by #2418
Labels
type: bug Something isn't correct or isn't working
Milestone

Comments

@Huggies23
Copy link

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

@Huggies23 Huggies23 added the TRIAGE Default label for untriaged issues label Nov 19, 2019
@philippjfr philippjfr added type: bug Something isn't correct or isn't working and removed TRIAGE Default label for untriaged issues labels Nov 25, 2019
@malekop
Copy link

malekop commented Dec 17, 2019

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":

Screenshot 2019-12-17 at 12 18 35

This panel contains the following elements:

  1. Tabs A,B,C.

    Each tab contains an identical GridSpec which consists of a:

  2. Bar plot produced using hvplot (fig1).

  3. 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.

  4. 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":

Screenshot 2019-12-17 at 12 19 04

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:

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:

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]:

     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:

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"})
# 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:

# 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())
# Identify Attribute to Link Map & Data (It is "lad19cd"):
shapes = cartopy.io.shapereader.Reader(shapefile)
list(shapes.records())[0]
# 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:

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:

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

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:

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:

example = pn.Tabs(("A",gspec),("B",gspec),("C",gspec))
example.show()

Screenshot 2019-12-17 at 12 18 35

Final tabbed Gridspec panel for the Plotly version:

example2 = pn.Tabs(("A",gspec2),("B",gspec2),("C",gspec2))
example2.show()

Screenshot 2019-12-17 at 12 19 04

@philippjfr
Copy link
Member

@jonmmease Do you have any idea what might be happening here?

@malekop
Copy link

malekop commented Mar 4, 2020

Hi, is there any update on this issue? Thanks

@philippjfr
Copy link
Member

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.

@rithwikjc
Copy link

rithwikjc commented Apr 28, 2020

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

@rithwikjc
Copy link

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),))

@thomasbangels
Copy link

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

@mvirus1996
Copy link

mvirus1996 commented Jan 14, 2021

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.

@pedroarbs
Copy link

Hi, any progress on this issue?

@effect
Copy link

effect commented May 27, 2021

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.

@jbednar
Copy link
Member

jbednar commented May 27, 2021

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.

@effect
Copy link

effect commented May 27, 2021

@jbednar agree, not general interactive plots, but it seems specifically Plotly objects together with Tabs create an issue.

@philippjfr
Copy link
Member

I'll see if I can come up with a fix very soon.

@rithwikjc
Copy link

Hoping for fix to this issue soon. Plotly itself has tabs implemented in Dash.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: bug Something isn't correct or isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants