From 54e88845d484f35fc720c5fbccfd6c67d0ef2905 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Fri, 12 Jan 2024 11:39:27 +0300 Subject: [PATCH] Calculate coordinate mapping after drawing figure To calculate accurate coordinate mappings for matplotlib (& plotnine) plots in display coordinates, the figure should know the location of it's subplots. The location determined by the subplot parameters (subplot_params) and they come from one or more of these sources, listed from lowest to highest priority: 1. rcParams 2. plt.subplots(..., gridspec_kw={..., **subplot_params}) 3. fig.subplot_adjust(**subplot_params) 4. layout engine The layout engine has the last say as it dynamically calculates subplot_params and calls fig.subplot_adjust() to set them. There are two things to consider: 1. Only layout engines that are "adjust_compatible" calculate subplot_params 2. Matplotlib runs the layout engine only when drawing the figure This PR ensures that there is an "adjust_compatible" layout engine and the plot is drawn/saved before the coordinate mappings are calculated. closes has2k1/plotnine#738 --- shiny/render/_try_render_plot.py | 44 ++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/shiny/render/_try_render_plot.py b/shiny/render/_try_render_plot.py index 75f22b383..74b88edd8 100644 --- a/shiny/render/_try_render_plot.py +++ b/shiny/render/_try_render_plot.py @@ -159,16 +159,27 @@ def try_render_matplotlib( ) fig.set_dpi(ppi_out * pixelratio) - # Suppress the message `UserWarning: The figure layout has changed to tight` - with warnings.catch_warnings(): - warnings.filterwarnings( - action="ignore", - category=UserWarning, - message="The figure layout has changed to tight", + # Calculating accurate coordinate mappings requires that the layout engine + # (if there is one) adjusts the figure's subplot parameters. + # e.g. "tight" layout. + # When there is no layout engine, "tight" layout is often helpful + layout_engine = fig.get_layout_engine() + if layout_engine: + if not layout_engine.adjust_compatible: + # In most cases, this branch will override the constained layout. + # which is usually a very deliberate choice by the user + fig.set_layout_engine( # pyright: ignore[reportUnknownMemberType] + layout="tight" + ) + warnings.warn( + f"'{type(layout_engine)}' layout engine is not compatible with shiny. " + "The figure layout has been changed to tight.", + stacklevel=1, + ) + else: + fig.set_layout_engine( # pyright: ignore[reportUnknownMemberType] + layout="tight" ) - plt.tight_layout() # pyright: ignore[reportUnknownMemberType] - - coordmap = get_coordmap(fig) with io.BytesIO() as buf: fig.savefig( # pyright: ignore[reportUnknownMemberType] @@ -181,6 +192,10 @@ def try_render_matplotlib( data = base64.b64encode(buf.read()) data_str = data.decode("utf-8") + # Calculating accurate coordinate mappings requires the figure to be + # drawn/saved first, which runs the layout engine. + coordmap = get_coordmap(fig) + res: ImgData = { "src": "data:image/png;base64," + data_str, "width": width_attr, @@ -343,10 +358,6 @@ def try_render_plotnine( verbose=False, **kwargs, ) - coordmap = get_coordmap_plotnine( - x, - res.figure, # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType, reportGeneralTypeIssues] - ) res.figure.savefig( # pyright: ignore[reportUnknownMemberType, reportGeneralTypeIssues] **res.kwargs # pyright: ignore[reportUnknownMemberType, reportGeneralTypeIssues] ) @@ -354,6 +365,13 @@ def try_render_plotnine( data = base64.b64encode(buf.read()) data_str = data.decode("utf-8") + # Calculating accurate coordinate mappings requires the figure to be + # drawn/saved first, which runs the layout engine. + coordmap = get_coordmap_plotnine( + x, + res.figure, # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType, reportGeneralTypeIssues] + ) + res: ImgData = { "src": "data:image/png;base64," + data_str, "width": w_attr,