posit-dev / py-shiny

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

Plot Disappears on Window Resize in Shiny App #1267

Open Toozig opened 4 months ago

Toozig commented 4 months ago

I've hit a snag with plot rendering.

with ui.card(full_screen=True):
    @render.plot()
    def track_plot():
        get_cur_plot()

The issue is that whenever I resize the window, the plot disappears. However, setting@render.plot(width=500, height=200) keeps it stable.

I've tried various solutions without success.

Full code ```python from shiny.express import input, render, ui from shiny import reactive, req import matplotlib.pyplot as plt import numpy as np import pandas as pd import random MAX_SCORE_TFBS = 'maximal score TFBS per site' SHOW_SEQ = 'Show sequence' FAMILY = 'family' PEAK = 'peak' SOURCE = 'source' CHECKBOX = 'checkbox' N_LINES = 'n_lines' SCORE_THRESHOLD = 'score_threshold' family_list = ["Alice", "Bob", "Charlie"] peak_list = ["Peak1", "Peak2", "Peak3"] source_list = ['JASPAR','HOMER'] checkbox_options = [MAX_SCORE_TFBS, SHOW_SEQ] def get_random_data2(family, peak, source): random_int = random.randint(0, 100) random_int2 = random.randint(0, 100) #create random DF with 4 rows return pd.DataFrame({ 'family': [family]*4, 'score': [random.randint(0, 100), random.randint(0, 100), random.randint(0, 100), random.randint(0, 100)], 'peak': [peak]*4, 'source': [source + ' ' + str(i) for i in range(4)], 'maximal score TFBS per site': [random_int2, random_int2, random_int2, random_int2], 'Show sequence': ['Show sequence']*4 }) def get_update_list(family): fam_index = family_list.index(family) return peak_list[fam_index:] def get_score_min_max(source_name, peak_id): random_int = random.randint(0, 100) return random_int, random_int + 100 def get_random_data(family): random_int = random.randint(0, 100) return pd.DataFrame({ 'family': [family], 'score': [random_int], 'peak': ['Peak1'], 'source': ['JASPAR'], 'maximal score TFBS per site': [random_int], 'Show sequence': ['Show sequence'] }) ##score threshold vars min_score = 0 max_score = 100 ui.page_opts(title="Family Viewer", fillable=True) with ui.sidebar(): # "Sidebar (input)" ui.input_selectize(FAMILY, "Family", family_list) ui.input_selectize(PEAK, 'Peak', peak_list) ui.input_selectize(SOURCE, 'Source', source_list) ui.input_slider(SCORE_THRESHOLD, 'Score Threshold', min=min_score, max=max_score, step=1, value=min_score + max_score // 2) ui.input_checkbox_group(CHECKBOX, 'Options', checkbox_options) ui.input_numeric(N_LINES, 'Number of lines', 2) # this is how to change the peak list based on the family selection @reactive.effect def change_peak_list(): choices = get_update_list(input.family()) ui.update_selectize(PEAK, choices=choices) # this is to update the slider based on the source and peak selection @reactive.effect def change_score_threshold(): min_score, max_score = get_score_min_max(input.source(), input.peak()) ui.update_slider(SCORE_THRESHOLD, min=min_score, max=max_score, value=min_score + max_score // 2) @reactive.calc def update_family(): cur_fam_df = get_random_data(input.family()) return cur_fam_df @reactive.calc def update_peak_data(): cur_fam_df = get_random_data(input.peak()) return cur_fam_df @reactive.calc def update_variant_list(): cur_fam_df = get_random_data2(input.family(), input.peak(), input.source()) return cur_fam_df def get_plot(family, peak, source): # create a random matplotlib plot vased on chosen family # plt.figure(figsize=(10, 5)) x = np.linspace(0, 10, 100) y = np.sin(x) plt.plot(x, y) plt.title(f"Family {family}, source: {source}") plt.ylabel(f"peaks {peak}") return plt @reactive.Calc def get_cur_plot(): get_plot(input.family(), input.peak(), input.source()) with ui.layout_columns(col_widths=[6, 6, 12]): with ui.card(full_screen=True): ui.card_header("family data") @render.data_frame def family_details(): return render.DataGrid(update_family(), row_selection_mode="multiple") with ui.card(full_screen=True): ui.card_header("peak data") @render.data_frame def peak_details(): return render.DataGrid(update_peak_data(), row_selection_mode="single") with ui.card(full_screen=True): # ui.card_header("peak data") @render.plot() def track_plot(): get_cur_plot() with ui.layout_columns(col_widths=[6, 6, 12]): with ui.card(full_screen=True): ui.card_header("Peak variants") @render.data_frame def variant_list(): return render.DataGrid(update_variant_list(), row_selection_mode="single") @reactive.calc def update_variant_information(): req(input.variant_list_selected_rows()) cur_fam_df = get_random_data(str(input.variant_list_selected_rows())) return cur_fam_df with ui.card(full_screen=True): ui.card_header("variant information") @render.data_frame def variant_information(): return render.DataGrid(update_variant_information(), row_selection_mode="single") ```
cpsievert commented 4 months ago

The first draw of the plot happens to work via side-effects -- @render.plot is capturing the matplotlib side-effects that happen when get_plot gets executed (via get_cur_plot()).

Part of where things have gone wrong here is that you've used @reative.calc to decorate get_cur_plot(), which doesn't return a value. A reactive calculation should always return a value (because if it's return value doesn't change, downstream reactive calculations won't know to re-execute). If you intentionally don't want to return a value, you likely want a @reactive.effect, not @reactive.calc, since the former is used for it's side-effects, not it's return value.

All that being said, I don't think you need either of those decorators in this case, just do have your helper function (which doesn't need to return a value):

def draw_plot(family, peak, source):
    # create a random matplotlib plot vased on chosen family

    # plt.figure(figsize=(10, 5))
    x = np.linspace(0, 10, 100)
    y = np.sin(x)
    plt.plot(x, y)
    plt.title(f"Family {family}, source: {source}")
    plt.ylabel(f"peaks {peak}")

Then call it in the @render.plot:

@render.plot()
def track_plot():
    draw_plot(input.family(), input.peak(), input.source())