From d1acb55b7b6b3eb9125650f8a1c6a0c87e9406ad Mon Sep 17 00:00:00 2001 From: Carson Date: Mon, 29 Jan 2024 15:57:14 -0600 Subject: [PATCH] Expressify, simplify, and improve UI for plot interaction examples --- examples/python/plot_interact_basic/app.py | 90 +++-------- examples/python/plot_interact_exclude/app.py | 132 +++++++-------- examples/python/plot_interact_select/app.py | 161 +++++++------------ 3 files changed, 138 insertions(+), 245 deletions(-) diff --git a/examples/python/plot_interact_basic/app.py b/examples/python/plot_interact_basic/app.py index feaa4286..71d219c9 100644 --- a/examples/python/plot_interact_basic/app.py +++ b/examples/python/plot_interact_basic/app.py @@ -3,90 +3,52 @@ import matplotlib.pyplot as plt import pandas as pd -from shiny import App, render, ui +from shiny.express import input, output_args, render, ui mtcars = pd.read_csv(Path(__file__).parent / "mtcars.csv") mtcars.drop(["disp", "hp", "drat", "qsec", "vs", "gear", "carb"], axis=1, inplace=True) +with ui.sidebar(): + ui.input_radio_buttons( + "plot_type", "Plot type", ["matplotlib", "plotnine"] + ) -app_ui = ui.page_fluid( - ui.head_content( - ui.tags.style( - """ - /* Smaller font for preformatted text */ - pre, table.table { - font-size: smaller; - } - pre, table.table { - font-size: smaller; - } - """ - ) - ), - ui.row( - ui.column( - 4, - ui.panel_well( - ui.input_radio_buttons( - "plot_type", "Plot type", ["matplotlib", "plotnine"] - ) - ), - ), - ui.column( - 8, - ui.output_plot("plot1", click=True, dblclick=True, hover=True, brush=True), - ), - ), - ui.row( - ui.column(3, ui.output_text_verbatim("click_info")), - ui.column(3, ui.output_text_verbatim("dblclick_info")), - ui.column(3, ui.output_text_verbatim("hover_info")), - ui.column(3, ui.output_text_verbatim("brush_info")), - ), -) +@output_args(click=True, dblclick=True, hover=True, brush=True) +@render.plot(alt="A scatterplot") +def plot1(): + if input.plot_type() == "matplotlib": + fig, ax = plt.subplots() + plt.title("Good old mtcars") + ax.scatter(mtcars["wt"], mtcars["mpg"]) + return fig + elif input.plot_type() == "plotnine": + from plotnine import aes, geom_point, ggplot, ggtitle -def server(input, output, session): - @output - @render.plot(alt="A scatterplot") - def plot1(): - if input.plot_type() == "matplotlib": - fig, ax = plt.subplots() - plt.title("Good old mtcars") - ax.scatter(mtcars["wt"], mtcars["mpg"]) - return fig + p = ( + ggplot(mtcars, aes("wt", "mpg")) + + geom_point() + + ggtitle("Good old mtcars") + ) - elif input.plot_type() == "plotnine": - from plotnine import aes, geom_point, ggplot, ggtitle + return p - p = ( - ggplot(mtcars, aes("wt", "mpg")) - + geom_point() - + ggtitle("Good old mtcars") - ) - return p +with ui.layout_column_wrap(): - @output - @render.text() + @render.code def click_info(): return "click:\n" + json.dumps(input.plot1_click(), indent=2) - @output - @render.text() + @render.code def dblclick_info(): return "dblclick:\n" + json.dumps(input.plot1_dblclick(), indent=2) - @output - @render.text() + @render.code def hover_info(): return "hover:\n" + json.dumps(input.plot1_hover(), indent=2) - @output - @render.text() + @render.code def brush_info(): return "brush:\n" + json.dumps(input.plot1_brush(), indent=2) - - -app = App(app_ui, server, debug=True) diff --git a/examples/python/plot_interact_exclude/app.py b/examples/python/plot_interact_exclude/app.py index 9f5aab2b..de5e7fc8 100644 --- a/examples/python/plot_interact_exclude/app.py +++ b/examples/python/plot_interact_exclude/app.py @@ -4,89 +4,67 @@ import pandas as pd import statsmodels.api as sm from plotnine import aes, geom_point, geom_smooth, ggplot -from shiny import App, reactive, render, ui + +from shiny import reactive +from shiny.express import input, output_args, render, ui from shiny.plotutils import brushed_points, near_points mtcars = pd.read_csv(Path(__file__).parent / "mtcars.csv") mtcars.drop(["disp", "hp", "drat", "qsec", "vs", "gear", "carb"], axis=1, inplace=True) +@output_args(click=True, brush=True) +@render.plot +def plot1(): + df = data_with_keep() + df_keep = df[df["keep"]] + df_exclude = df[~df["keep"]] + + return ( + ggplot(df_keep, aes("wt", "mpg")) + + geom_point() + + geom_point(data=df_exclude, color="#666", fill="white") + + geom_smooth(method="lm", fullrange=True) + ) + -app_ui = ui.page_fluid( - ui.head_content( - ui.tags.style( - """ - pre, table.table { - font-size: smaller; - } - """ - ) - ), - ui.row( - ui.column(2), - ui.column( - 8, - ui.output_plot("plot1", click=True, brush=True), - ui.div( - {"style": "text-align: center"}, - ui.input_action_button("exclude_toggle", "Toggle brushed points"), - ui.input_action_button("exclude_reset", "Reset"), - ), - ), - ), - ui.row( - ui.column(12, {"style": "margin-top: 15px;"}, ui.output_text_verbatim("model")), - ), +ui.div( + ui.input_action_button("exclude_toggle", "Toggle brushed points"), + ui.input_action_button("exclude_reset", "Reset"), + class_="d-flex justify-content-center pb-3 gap-3", ) -def server(input, output, session): - keep_rows = reactive.Value([True] * len(mtcars)) - - @reactive.Calc - def data_with_keep(): - df = mtcars.copy() - df["keep"] = keep_rows() - return df - - @reactive.Effect - @reactive.event(input.plot1_click) - def _(): - res = near_points(mtcars, input.plot1_click(), all_rows=True, max_points=1) - keep_rows.set(list(np.logical_xor(keep_rows(), res.selected_))) - - @reactive.Effect - @reactive.event(input.exclude_toggle) - def _(): - res = brushed_points(mtcars, input.plot1_brush(), all_rows=True) - keep_rows.set(list(np.logical_xor(keep_rows(), res.selected_))) - - @reactive.Effect - @reactive.event(input.exclude_reset) - def _(): - keep_rows.set([True] * len(mtcars)) - - @output - @render.plot() - def plot1(): - df = data_with_keep() - df_keep = df[df["keep"]] - df_exclude = df[~df["keep"]] - - return ( - ggplot(df_keep, aes("wt", "mpg")) - + geom_point() - + geom_point(data=df_exclude, color="#666", fill="white") - + geom_smooth(method="lm", fullrange=True) - ) - - @output - @render.text() - def model(): - df = data_with_keep() - df_keep = df[df["keep"]] - mod = sm.OLS(df_keep["wt"], df_keep["mpg"]) - res = mod.fit() - return res.summary() - - -app = App(app_ui, server) +@render.code +def model(): + df = data_with_keep() + df_keep = df[df["keep"]] + mod = sm.OLS(df_keep["wt"], df_keep["mpg"]) + res = mod.fit() + return res.summary() + + +@reactive.calc +def data_with_keep(): + df = mtcars.copy() + df["keep"] = keep_rows() + return df + + +keep_rows = reactive.value([True] * len(mtcars)) + +@reactive.effect +@reactive.event(input.plot1_click) +def _(): + res = near_points(mtcars, input.plot1_click(), all_rows=True, max_points=1) + keep_rows.set(list(np.logical_xor(keep_rows(), res.selected_))) + +@reactive.effect +@reactive.event(input.exclude_toggle) +def _(): + res = brushed_points(mtcars, input.plot1_brush(), all_rows=True) + keep_rows.set(list(np.logical_xor(keep_rows(), res.selected_))) + +@reactive.effect +@reactive.event(input.exclude_reset) +def _(): + keep_rows.set([True] * len(mtcars)) diff --git a/examples/python/plot_interact_select/app.py b/examples/python/plot_interact_select/app.py index 74be4c03..663ad215 100644 --- a/examples/python/plot_interact_select/app.py +++ b/examples/python/plot_interact_select/app.py @@ -1,9 +1,11 @@ from pathlib import Path import pandas as pd -from plotnine import aes, facet_grid, geom_point, ggplot -from shiny import App, render, ui +from plotnine import aes, facet_grid, geom_point, ggplot, ggtitle, theme_minimal + +from shiny.express import input, render, ui from shiny.plotutils import brushed_points, near_points +from shiny.ui import output_plot mtcars = pd.read_csv(Path(__file__).parent / "mtcars.csv") mtcars.drop(["disp", "hp", "drat", "qsec", "vs", "gear", "carb"], axis=1, inplace=True) @@ -11,113 +13,64 @@ # In fast mode, throttle interval in ms. FAST_INTERACT_INTERVAL = 60 -app_ui = ui.page_fluid( - ui.head_content( - ui.tags.style( - """ - pre, table.table { - font-size: smaller; - } - """ - ) - ), - ui.row( - ui.column( - 4, - ui.panel_well( - ui.input_checkbox("facet", "Use facets", False), - ui.input_radio_buttons( - "brush_dir", "Brush direction", ["xy", "x", "y"], inline=True - ), - ui.input_checkbox( - "fast", - f"Fast hovering/brushing (throttled with {FAST_INTERACT_INTERVAL}ms interval)", - ), - ui.input_checkbox("all_rows", "Return all rows in data frame", False), - ui.input_slider( - "max_distance", "Max distance of point from hover", 1, 20, 5 - ), - ), - ), - ui.column( - 8, - ui.output_ui("plot_ui"), - ), - ), - ui.row( - ui.column(6, ui.tags.b("Points near cursor"), ui.output_table("near_hover")), - ui.column(6, ui.tags.b("Points in brush"), ui.output_table("in_brush")), - ), -) - - -def server(input, output, session): - @output - @render.ui - def plot_ui(): - hover_opts_kwargs = {} - brush_opts_kwargs = {} - brush_opts_kwargs["direction"] = input.brush_dir() - - if input.fast(): - hover_opts_kwargs["delay"] = FAST_INTERACT_INTERVAL - hover_opts_kwargs["delay_type"] = "throttle" - brush_opts_kwargs["delay"] = FAST_INTERACT_INTERVAL - brush_opts_kwargs["delay_type"] = "throttle" +with ui.sidebar(title="Interaction options"): + ui.input_checkbox("facet", "Use facets", False) + ui.input_radio_buttons("brush_dir", "Brush direction", ["xy", "x", "y"], inline=True) + ui.input_checkbox("fast", f"Fast hovering/brushing (throttled with {FAST_INTERACT_INTERVAL}ms interval)") + ui.input_checkbox("all_rows", "Return all rows in data frame", False) + ui.input_slider("max_distance", "Max distance of point from hover", 1, 20, 5) - return ui.output_plot( - "plot1", - hover=ui.hover_opts(**hover_opts_kwargs), - brush=ui.brush_opts(**brush_opts_kwargs), - ) - - @output - @render.plot() +with ui.hold(): + @render.plot def plot1(): - p = ggplot(mtcars, aes("wt", "mpg")) + geom_point() + p = ( + ggplot(mtcars, aes("wt", "mpg")) + geom_point() + theme_minimal() + + ggtitle("Hover over points or click + drag to brush") + ) if input.facet(): p = p + facet_grid("am~cyl") - return p - @output - @render.table() - def near_hover(): - return near_points( - mtcars, - input.plot1_hover(), - threshold=input.max_distance(), - add_dist=True, - all_rows=input.all_rows(), - ) - - @output - @render.table() - def in_brush(): - return brushed_points( - mtcars, - input.plot1_brush(), - all_rows=input.all_rows(), - ) - - -app = App(app_ui, server) +@render.ui +def plot_ui(): + hover_opts_kwargs = {} + brush_opts_kwargs = {} + brush_opts_kwargs["direction"] = input.brush_dir() + + if input.fast(): + hover_opts_kwargs["delay"] = FAST_INTERACT_INTERVAL + hover_opts_kwargs["delay_type"] = "throttle" + brush_opts_kwargs["delay"] = FAST_INTERACT_INTERVAL + brush_opts_kwargs["delay_type"] = "throttle" + + return output_plot( + "plot1", + hover=ui.hover_opts(**hover_opts_kwargs), + brush=ui.brush_opts(**brush_opts_kwargs), + ) -def format_table(df: pd.DataFrame): - return ( - df.style.set_table_attributes('class="dataframe shiny-table table w-auto"') - .hide(axis="index") # pyright: reportUnknownMemberType=false - .set_table_styles( - [ - dict(selector="th", props=[("text-align", "right")]), - dict( - selector="tr>td", - props=[ - ("padding-top", "0.1rem"), - ("padding-bottom", "0.1rem"), - ], - ), - ] # pyright: reportGeneralTypeIssues=false - ) - ) +with ui.layout_columns(): + with ui.card(): + ui.card_header("Points near cursor") + + @render.data_frame + def near_hover(): + return near_points( + mtcars, + input.plot1_hover(), + threshold=input.max_distance(), + add_dist=True, + all_rows=input.all_rows(), + ) + + with ui.card(): + ui.card_header("Points in brush") + + @render.data_frame + def in_brush(): + return brushed_points( + mtcars, + input.plot1_brush(), + all_rows=input.all_rows(), + )