posit-dev / py-shiny

Shiny for Python
https://shiny.posit.co/py/
MIT License
1.24k stars 70 forks source link

Nested tabs (navs) sometimes cause output_widget not to render #387

Open bartverweire opened 1 year ago

bartverweire commented 1 year ago

Hello,

I'm having an issue with a Shiny application with nested navs, and a plotly figure using shinywidgets. Because the issue only (?) occurs when nested navs are involved, I suspect this is a shiny issue, and not a shinywidgets issue.

I have narrowed down the problem to the following sample program. The app is constructed as a page_navbar, with 2 tabs, "Nav A" and "Nav B". "Nav A" is built as a navset_tab with two subtabs, "SubNav A" and "SubNav B". "SubNav A" contains a first output_widget, i.e. this is an output_widget on level 2 of the nav tree. "Nav B" is containing a second output_widget. This is an output_widget on level 1 of the nav tree.

The weird thing is that the first output_widget (level 2) is only displayed if the second output_widget (level 1) exists. If I comment out the second output_widget, the first one is not rendered. The second output_widget doesn't even need to be rendered. Just having the output_widget in the ui is enough to be able to render the first one.

Here's a sample program demonstrating the issue (you need to play with comments on the output_widget from Section 2).

from shiny import *
from shinywidgets import *

import pandas as pd
import numpy as np
import plotly.express as px
import plotly.graph_objects as go

df = pd.DataFrame(np.random.randn(50, 3), index=range(50), columns=["x", "y", "z"])

# App consists of a top navigation bar, with 2 navigation items "Nav A" and "Nav B"
app_ui = ui.page_navbar(
    ui.nav(
        "Nav A",
        # Section 1 : nested navset, with one nav containing an output_widget
        # This only renders correctly if there's an output_widget in the "Nav B" tab
        ui.navset_tab(
            ui.nav("SubNav A",
                   ui.h3("Plotly widget 1"),
                   output_widget("out_plotly_widget1"),
                   ),
            ui.nav("SubNav B")
        ),
    ),
    ui.nav(
        "Nav B",
        # Section 2 : the output_widget must exist for the first output_widget (above) to render
        ui.h3("Plotly widget 2"),
        output_widget("out_plotly_widget2"),
    ),
    ui.nav_control(
        ui.input_select("in_selection", label=None, choices=[1,2,3,4])
    ),
)

def server(input, output, session):
    selected_value = reactive.Value()

    @reactive.Effect
    def _():
        req(input.in_selection())

        selected_value.set(input.in_selection())

    @output
    @render_widget
    def out_plotly_widget1():
        fig = px.scatter(df, x="x", y="y")

        return go.FigureWidget(fig)

    # Only for illustrative reasons.
    # Doesn't make a difference if this rendering function exists or not
    # Can be commented out without any impact
    @output
    @render_widget
    def out_plotly_widget2():
        fig = px.scatter(df, x="y", y="z")

        return go.FigureWidget(fig)

app = App(app_ui, server)

So this works :

This doesn't work :

In a further test, I also inluded a navset_tab for "Nav B". It appears that it's not enough to have an output widget defined on level 2. There needs to be a widget on level 1 (dummy or not).

In the following code snippet, I moved out_plotly_widget_2 one level down, and added a dummy widget in "Nav B".

from shiny import *
from shinywidgets import *

import pandas as pd
import numpy as np
import plotly.express as px
import plotly.graph_objects as go

df = pd.DataFrame(np.random.randn(50, 3), index=range(50), columns=["x", "y", "z"])

# App consists of a top navigation bar, with 2 navigation items "Nav A" and "Nav B"
app_ui = ui.page_navbar(
    ui.nav(
        "Nav A",
        # Section 1 : nested navset, with one nav containing an output_widget
        # This only renders correctly if there's an output_widget in the "Nav B" tab
        ui.navset_tab(
            ui.nav("SubNav AA",
                   ui.h3("Plotly widget 1"),
                   output_widget("out_plotly_widget1"),
                   ),
            ui.nav("SubNav AB")
        ),
    ),
    ui.nav(
        "Nav B",
        # Section 2 : the output_widget must exist for the first output_widget (above) to render
        ui.navset_tab(
            ui.nav("SubNav BB",
                   ui.h3("Plotly widget 2"),
                   output_widget("out_plotly_widget2"),
                   ),
            ui.nav("SubNav BB")
        ),
        # ui.h3("Dummy widget"),
        output_widget("dummy widget"),
    )
)

def server(input, output, session):
    selected_value = reactive.Value()

    @reactive.Effect
    def _():
        req(input.in_selection())

        selected_value.set(input.in_selection())

    @output
    @render_widget
    def out_plotly_widget1():
        fig = px.scatter(df, x="x", y="y")

        return go.FigureWidget(fig)

    # Only for illustrave reasons.
    # Doesn't make a difference if this rendering function exists or not
    # Can be commented out without any impact
    @output
    @render_widget
    def out_plotly_widget2():
        fig = px.scatter(df, x="y", y="z")

        return go.FigureWidget(fig)

app = App(app_ui, server, debug=True)

It doesn't seem to matter where you put the dummy widget (Nav A or Nav B).

So there seems to be a workaround ...

Thanks for having a look.

Bart

bartverweire commented 1 year ago

Forgot to mention the versions : shiny: 0.2.9 shinywidgets : 0.1.4