diff --git a/Makefile b/Makefile index d780d7de7..e094130b4 100644 --- a/Makefile +++ b/Makefile @@ -57,7 +57,10 @@ typings/matplotlib/__init__.pyi: ## grab type stubs from GitHub mv typings/python-type-stubs/stubs/matplotlib typings/ rm -rf typings/python-type-stubs -pyright: typings/uvicorn typings/matplotlib/__init__.pyi ## type check with pyright +typings/seaborn: + pyright --createstub seaborn + +pyright: typings/uvicorn typings/matplotlib/__init__.pyi typings/seaborn ## type check with pyright pyright lint: ## check style with flake8 diff --git a/shiny/render/_try_render_plot.py b/shiny/render/_try_render_plot.py index b076427c5..490413139 100644 --- a/shiny/render/_try_render_plot.py +++ b/shiny/render/_try_render_plot.py @@ -104,17 +104,16 @@ def _get_img_size_px( # size to that exactly. We assume there's some reason they wanted that exact size. user_specified_size_px = self.user_specified_size_px[i] if user_specified_size_px is not None: - return user_specified_size_px, f"{user_specified_size_px}px" - - # If they specified a figure size in their plotting code, we'll respect that. - if abs(fig_initial_size_inches - fig_result_size_inches) > 1e-6: - native_size = fig_result_size_inches * dpi - return native_size, f"{native_size}px" - - # If the user didn't specify an explicit size on @render.plot and didn't modify - # the figure size in their plotting code, then assume that they're filling the - # container, in which case we set the img size to 100% in order to have nicer - # resize behavior. + if user_specified_size_px == 0: + # If the explicit size is 0, we'll respect the user's figure size. + native_size = fig_result_size_inches * dpi + return native_size, f"{native_size}px" + else: + return user_specified_size_px, f"{user_specified_size_px}px" + + # If the user didn't specify an explicit size on @render.plot then assume that + # they're filling the container, in which case we set the img size to 100% in + # order to have nicer resize behavior. # # Retrieve the container size, taking a reactive dependency container_size_px = self._container_size_px_fn[i]() diff --git a/tests/e2e/plot-sizing/app.py b/tests/e2e/plot-sizing/app.py index 49ec04850..a16fd7b43 100644 --- a/tests/e2e/plot-sizing/app.py +++ b/tests/e2e/plot-sizing/app.py @@ -6,13 +6,17 @@ import matplotlib.pyplot as plt import numpy as np +import seaborn as sns from PIL import Image -from plotnine import aes, element_rect, facet_wrap, geom_point, stat_smooth, theme +from plotnine import aes, element_rect, geom_point, theme, theme_minimal from plotnine.data import mtcars from plotnine.ggplot import ggplot from shiny import App, Inputs, Outputs, Session, module, render, req, ui +tips = sns.load_dataset("tips") +dpi = 150 + @module.ui def plot_ui(): @@ -51,7 +55,7 @@ def plot_dom_size(): def plot_decorator_size(): return plot_fn(None) - @render.plot + @render.plot(width=0, height=0) def plot_native_size(): return plot_fn((300, 200)) @@ -65,12 +69,19 @@ def plot_native_size(): plot_ui("mpl"), value="mpl", ), + ui.nav( + "seaborn", + ui.p( + "The following four plots should all be the same size. The last one should have larger text." + ), + plot_ui("sns"), + value="sns", + ), ui.nav( "plotnine", ui.p( "The following four plots should all be the same size. The last one should have larger text." ), - ui.p("It may take a moment for the plots to render."), plot_ui("plotnine"), ), ui.nav( @@ -99,19 +110,31 @@ def plot_with_mpl(fig_size: tuple[float, float] | None) -> object: return fig + def plot_with_sns(fig_size: tuple[float, float] | None) -> object: + kwargs = dict() + if fig_size: + kwargs["height"] = fig_size[1] / dpi + kwargs["aspect"] = fig_size[0] / fig_size[1] + + # FacetGrid has an opinion about its figure size + g = sns.FacetGrid(tips, **kwargs) # pyright: ignore[reportUnknownArgumentType] + g.figure.set_facecolor("lavender") + g.map(sns.scatterplot, "total_bill", "tip") + plt.gca().set_facecolor("lavender") + if fig_size: + plt.gcf().set_dpi(dpi) + def plot_with_plotnine(fig_size: tuple[float, float] | None) -> object: p = ( - ggplot(mtcars, aes("wt", "mpg", color="factor(gear)")) + ggplot(mtcars, aes("wt", "mpg")) + geom_point() - + stat_smooth(method="lm") - + facet_wrap("~gear") + + theme_minimal() + theme( plot_background=element_rect(fill="lavender"), legend_background=element_rect(fill="lavender"), ) ) if fig_size is not None: - dpi = 150 p = p + theme( figure_size=(fig_size[0] / dpi, fig_size[1] / dpi), plot_background=element_rect(fill="lavender"), @@ -124,6 +147,7 @@ def plot_with_pil(fig_size: tuple[float, float] | None) -> object: return Image.open(Path(__file__).parent / "bike.jpg") plot_server("mpl", plot_with_mpl) + plot_server("sns", plot_with_sns) plot_server("plotnine", plot_with_plotnine) plot_server("pil", plot_with_pil) diff --git a/tests/e2e/plot-sizing/test_plot_sizing.py b/tests/e2e/plot-sizing/test_plot_sizing.py index 5d8fdfb1d..eb9ad2962 100644 --- a/tests/e2e/plot-sizing/test_plot_sizing.py +++ b/tests/e2e/plot-sizing/test_plot_sizing.py @@ -15,6 +15,12 @@ def test_output_image_kitchen(page: Page, local_app: ShinyAppProc) -> None: "mpl-plot_decorator_size", "mpl-plot_native_size", ], + "sns": [ + "sns-plot_default", + "sns-plot_dom_size", + "sns-plot_decorator_size", + "sns-plot_native_size", + ], "plotnine": [ "plotnine-plot_default", "plotnine-plot_dom_size", @@ -40,8 +46,12 @@ def test_output_image_kitchen(page: Page, local_app: ShinyAppProc) -> None: rect = page.evaluate( f"() => document.querySelector('#{plotid} img').getBoundingClientRect()" ) - assert abs(rect["width"] - 300) < 1e-4 - assert abs(rect["height"] - 200) < 1e-4 + tolerance = 1e-4 + if plotid.startswith("sns"): + # Not sure why but sns native-sized plot is a bit off + tolerance += 2 + assert abs(rect["width"] - 300) <= tolerance + assert abs(rect["height"] - 200) <= tolerance def test_decorator_passthrough_size():