0

I have been trying, unsuccessfully, to reproduce this minimal R Shiny app in PyShiny. I tried to literally translate the R code to python code, but it looks like I need to dynamically name the plotname() function inside the @render.plot decorator. All my attempts do not return any plot. I know the problem is with the exec() line near the bottom. How do I dynamically change the function name in the loop? Any help on fixing the code below:

from shiny import App, render, ui
import matplotlib.pyplot as plt

#========== helper function =======================
def do_call(what, args=[], kwargs = {}): # code picked from https://stackoverflow.com/questions/38722804/equivalent-to-rs-do-call-in-python
    return what(*args, **kwargs)
#=================================================
max_plots = 5

app_ui = ui.page_fluid(
    ui.input_slider("n", "Number of plots", value=1, min=1, max=5),
    ui.output_ui("plots")
)

def server(input, output, session):
     
    @output
    @render.ui
    def plots():
        plot_output_list = []
        for i in range(1, input.n()+1):
            plotname = f"plot{i}"
            
            plot_output_list.append(ui.output_plot(plotname))
        return do_call(ui.TagList, plot_output_list)

    for j in range(1, max_plots+1):
        #my_i = j
        #plotname = f"plot{my_i}"
        @output
        @render.plot
        def exec(f"plot{j}")(): # this line is not correct
            fig = plt.plot(range(1, j+1), range(1, j+1))
            return fig

app = App(app_ui, server)
sbik
  • 41
  • 7
  • I can't get py-shiny running... but I think the problem could be with `def plotname():` which creates the same function on each of your iterations. Changing that to `exec(f"def plotname_{j}(): )` should create plotname_1 etc. Does that help at all? – smartse Jul 24 '23 at 22:05
  • Thank you smartse. plotname is dynamically created, so I tried `exec(f"def {plotname}():")`, but it is not working. Your suggestion looks promising; I will play around with it. – sbik Jul 24 '23 at 22:23
  • You mean with `plotname = f"plot{my_i}"`? that's not going to work. Put `plot{my_i}` inside the `exec` – smartse Jul 24 '23 at 22:47
  • the `def` needs to be inside the `exec`. Not sure on this, but I think the whole function needs wrapping inside `exec` i.e. the closing bracket after `return fig` – smartse Jul 25 '23 at 14:23
  • If I can get something like the following to work, I will be able to carry on: ```i = 2 def exec(f"plot{i}")(x, y): return x+y```. Wrapping the function in `exec` does not seem to work. – sbik Jul 25 '23 at 15:18

3 Answers3

1

This solution correctly captures the context for each plot (each plot is different -- look at the axes). The original code used a single closure, defined in the loop, so all 5 graphs were the same.

The do_call helper is unnecessary -- just call ui.tagList().

Note that you could also invoke the render.plot directly in render_plot_func instead of using the decorator, i.e. return render.plot(f) and remove @render.plot. In the case of output, you cannot use the decorator syntax because there's no function definition to annotate there.

from shiny import App, render, ui
import matplotlib.pyplot as plt

# =================================================
max_plots = 5

app_ui = ui.page_fluid(
    ui.input_slider("n", "Number of plots", value=1, min=1, max=5),
    ui.output_ui("plots")
)


def server(input, output, session):
    def render_plot_func(j):
        @render.plot
        def f():
            fig = plt.plot(range(1, j + 1), range(1, j + 1))
            return fig
        return f
        
    @output
    @render.ui
    def plots():
        plot_output_list = []
        for i in range(1, input.n() + 1):
            plotname = f"plot{i}"

            plot_output_list.append(ui.output_plot(plotname))
            output(render_plot_func(i), id=plotname)
        return ui.TagList(plot_output_list)

app = App(app_ui, server)

@sbik: your answer to your own question looks effective, but I would suggest that that this version (without the exec call) is to be preferred, for readability if nothing else.

Scott Cooper
  • 71
  • 1
  • 4
0
from shiny import App, render, ui
import matplotlib.pyplot as plt


## ========== helper function =======================##
def do_call(what, args=[],
            kwargs={}):  # code picked from https://stackoverflow.com/questions/38722804/equivalent-to-rs-do-call-in-python
    return what(*args, **kwargs)


# =================================================
max_plots = 5

app_ui = ui.page_fluid(
    ui.input_slider("n", "Number of plots", value=1, min=1, max=5),
    ui.output_ui("plots")
)


def server(input, output, session):
    @output
    @render.ui
    def plots():
        plot_output_list = []
        for i in range(1, input.n() + 1):
            plotname = f"plot{i}"

            plot_output_list.append(ui.output_plot(plotname))
        return do_call(ui.TagList, plot_output_list)

    for j in range(1, max_plots + 1):
        @output(f"plot{j}")
        @render.plot
        def exec():
            fig = plt.plot(range(1, j + 1), range(1, j + 1))
            return fig

app = App(app_ui, server)
  • Thank you harsha! I your code, and get the following error. `_send_error_response: 'str' object has no attribute '__name__'`. Any idea what may be causing the error? Thank you – sbik Jul 31 '23 at 19:54
  • @sbik try with `@output(id=f"plot{j}")` – David Emanuel Sandoval Aug 01 '23 at 03:15
  • The other problem ocurred because by default it expects a function, and functions have names, so you can provide an id (str) instead – David Emanuel Sandoval Aug 01 '23 at 03:21
  • That should work with the code in the asnwer of @harsha – David Emanuel Sandoval Aug 01 '23 at 03:23
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Aug 04 '23 at 04:47
0

I did finally get a solution that works. I am posting the solution in case someone else needs it.


from shiny import App, render, ui
import matplotlib.pyplot as plt


#========== helper function =======================
def do_call(what, args=[], kwargs = {}): # code picked from https://stackoverflow.com/questions/38722804/equivalent-to-rs-do-call-in-python
    return what(*args, **kwargs)
#=================================================
max_plots = 5

app_ui = ui.page_fluid(
    ui.input_slider("n", "Number of plots", value=1, min=1, max=5),
    ui.output_ui("plots")
)

def server(input, output, session):
     
    @output
    @render.ui
    def plots():
        plot_output_list = []
        for i in range(1, input.n()+1):
            plotname = f"plot{i}"
            
            plot_output_list.append(ui.output_plot(plotname))
        return do_call(ui.TagList, plot_output_list)

    for j in range(1, max_plots+1):
        exec(f"@output\n@render.plot\ndef plot{j}():\n    fig = plt.plot(range(1, {j+1}), range(1, {j+1}))\n    return fig")

app = App(app_ui, server)
sbik
  • 41
  • 7
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Aug 05 '23 at 05:13