holoviz / panel

Panel: The powerful data exploration & web app framework for Python
https://panel.holoviz.org
BSD 3-Clause "New" or "Revised" License
4.7k stars 510 forks source link

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

Closed Huggies23 closed 3 years ago

Huggies23 commented 4 years ago

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

malekop commented 4 years ago

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:


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 commented 4 years ago

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

malekop commented 4 years ago

Hi, is there any update on this issue? Thanks

philippjfr commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 3 years ago

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 commented 3 years ago

Hi, any progress on this issue?

effect commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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

philippjfr commented 3 years ago

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

rithwikjc commented 3 years ago

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