diff --git a/doc/user-guide/options.rst b/doc/user-guide/options.rst index f55348f825c..bb1f17ec6fd 100644 --- a/doc/user-guide/options.rst +++ b/doc/user-guide/options.rst @@ -17,7 +17,7 @@ Xarray offers a small number of configuration options through :py:func:`set_opti - ``display_style`` 2. Control behaviour during operations: ``arithmetic_join``, ``keep_attrs``, ``use_bottleneck``. -3. Control colormaps for plots:``cmap_divergent``, ``cmap_sequential``. +3. Control plotting: ``cmap_divergent``, ``cmap_sequential``, ``facetgrid_figsize``. 4. Aspects of file reading: ``file_cache_maxsize``, ``netcdf_engine_order``, ``warn_on_unclosed_files``. diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 1116940a4cc..28bca505a02 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -172,6 +172,11 @@ Antonio Valentino, Chris Barker, Christine P. Chai, Deepak Cherian, Ewan Short, New Features ~~~~~~~~~~~~ +- Added ``facetgrid_figsize`` option to :py:func:`~xarray.set_options` allowing + :py:class:`~xarray.plot.FacetGrid` to use ``matplotlib.rcParams['figure.figsize']`` + or a fixed ``(width, height)`` tuple instead of computing figure size from + ``size`` and ``aspect`` (:issue:`11103`). + By `Kristian Kollsga `_. - :py:class:`~xarray.indexes.NDPointIndex` now supports coordinates with fewer dimensions than coordinate variables, enabling indexing of scattered points and trajectories where multiple coordinates (e.g., ``x``, ``y``) share a diff --git a/xarray/core/options.py b/xarray/core/options.py index 2d910d80d65..b43e4ff3b09 100644 --- a/xarray/core/options.py +++ b/xarray/core/options.py @@ -40,6 +40,7 @@ "use_numbagg", "use_opt_einsum", "use_flox", + "facetgrid_figsize", ] class T_Options(TypedDict): @@ -73,6 +74,7 @@ class T_Options(TypedDict): use_new_combine_kwarg_defaults: bool use_numbagg: bool use_opt_einsum: bool + facetgrid_figsize: Literal["computed", "rcparams"] | tuple[float, float] OPTIONS: T_Options = { @@ -106,8 +108,10 @@ class T_Options(TypedDict): "use_new_combine_kwarg_defaults": False, "use_numbagg": True, "use_opt_einsum": True, + "facetgrid_figsize": "computed", } +_FACETGRID_FIGSIZE_OPTIONS = frozenset(["computed", "rcparams"]) _JOIN_OPTIONS = frozenset(["inner", "outer", "left", "right", "exact"]) _DISPLAY_OPTIONS = frozenset(["text", "html"]) _NETCDF_ENGINES = frozenset(["netcdf4", "h5netcdf", "scipy"]) @@ -144,6 +148,14 @@ def _positive_integer(value: Any) -> bool: "use_opt_einsum": lambda value: isinstance(value, bool), "use_flox": lambda value: isinstance(value, bool), "warn_for_unclosed_files": lambda value: isinstance(value, bool), + "facetgrid_figsize": lambda value: ( + value in _FACETGRID_FIGSIZE_OPTIONS + or ( + isinstance(value, tuple) + and len(value) == 2 + and all(isinstance(v, (int, float)) for v in value) + ) + ), } @@ -222,6 +234,15 @@ class set_options: chunk_manager : str, default: "dask" Chunk manager to use for chunked array computations when multiple options are installed. + facetgrid_figsize : {"computed", "rcparams"} or tuple of float, default: "computed" + How :class:`~xarray.plot.FacetGrid` determines figure size when + ``figsize`` is not explicitly passed: + + * ``"computed"`` : figure size is derived from ``size`` and ``aspect`` + parameters (current default behavior). + * ``"rcparams"`` : use ``matplotlib.rcParams['figure.figsize']`` as the + total figure size. + * ``(width, height)`` : use a fixed figure size (in inches). cmap_divergent : str or matplotlib.colors.Colormap, default: "RdBu_r" Colormap to use for divergent data plots. If string, must be matplotlib built-in colormap. Can also be a Colormap object @@ -357,6 +378,11 @@ def __init__(self, **kwargs): expected = f"Expected one of {_JOIN_OPTIONS!r}" elif k == "display_style": expected = f"Expected one of {_DISPLAY_OPTIONS!r}" + elif k == "facetgrid_figsize": + expected = ( + f"Expected one of {_FACETGRID_FIGSIZE_OPTIONS!r}" + " or a (width, height) tuple of floats" + ) elif k == "netcdf_engine_order": expected = f"Expected a subset of {sorted(_NETCDF_ENGINES)}" else: diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index 5da382c1177..16cc04b0fbc 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -195,6 +195,19 @@ def __init__( else: raise ValueError("Pass a coordinate name as an argument for row or col") + # Resolve figsize from global option before computing grid shape, + # so that downstream heuristics (e.g. col_wrap="auto") can use it. + if figsize is None: + from xarray.core.options import OPTIONS + + facetgrid_figsize = OPTIONS["facetgrid_figsize"] + if isinstance(facetgrid_figsize, tuple): + figsize = facetgrid_figsize + elif facetgrid_figsize == "rcparams": + import matplotlib as mpl + + figsize = tuple(mpl.rcParams["figure.figsize"]) + # Compute grid shape if single_group: nfacet = len(data[single_group]) @@ -212,8 +225,8 @@ def __init__( subplot_kws = {} if subplot_kws is None else subplot_kws if figsize is None: - # Calculate the base figure size with extra horizontal space for a - # colorbar + # Calculate the base figure size with extra horizontal space + # for a colorbar cbar_space = 1 figsize = (ncol * size * aspect + cbar_space, nrow * size) diff --git a/xarray/tests/test_options.py b/xarray/tests/test_options.py index ca9c1fd6440..fd05f9d5122 100644 --- a/xarray/tests/test_options.py +++ b/xarray/tests/test_options.py @@ -84,6 +84,21 @@ def test_netcdf_engine_order() -> None: assert OPTIONS["netcdf_engine_order"] == original +def test_facetgrid_figsize() -> None: + with pytest.raises(ValueError): + xarray.set_options(facetgrid_figsize="invalid") + with pytest.raises(ValueError): + xarray.set_options(facetgrid_figsize=(1.0,)) + with pytest.raises(ValueError): + xarray.set_options(facetgrid_figsize=(1.0, 2.0, 3.0)) + with xarray.set_options(facetgrid_figsize="rcparams"): + assert OPTIONS["facetgrid_figsize"] == "rcparams" + with xarray.set_options(facetgrid_figsize="computed"): + assert OPTIONS["facetgrid_figsize"] == "computed" + with xarray.set_options(facetgrid_figsize=(12.0, 8.0)): + assert OPTIONS["facetgrid_figsize"] == (12.0, 8.0) + + def test_display_style() -> None: original = "html" assert OPTIONS["display_style"] == original diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 5980d449dbb..b6805ec3957 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -3567,3 +3567,47 @@ def test_temp_dataarray() -> None: locals_ = dict(x="x", extend="var2") da = _temp_dataarray(ds, y_, locals_) assert da.shape == (3,) + + +@requires_matplotlib +def test_facetgrid_figsize_rcparams() -> None: + """Test that facetgrid_figsize='rcparams' uses matplotlib rcParams.""" + import matplotlib as mpl + + da = DataArray( + np.random.randn(10, 15, 3), + dims=["y", "x", "z"], + coords={"z": ["a", "b", "c"]}, + ) + custom_figsize = (12.0, 8.0) + + with figure_context(): + # Default behavior: computed from size and aspect + g = xplt.FacetGrid(da, col="z") + default_figsize = g.fig.get_size_inches() + # Default should be (ncol * size * aspect + cbar_space, nrow * size) + # = (3 * 3 * 1 + 1, 1 * 3) = (10, 3) + np.testing.assert_allclose(default_figsize, (10.0, 3.0)) + + with figure_context(): + # rcparams mode: should use mpl.rcParams['figure.figsize'] + with mpl.rc_context({"figure.figsize": custom_figsize}): + with xr.set_options(facetgrid_figsize="rcparams"): + g = xplt.FacetGrid(da, col="z") + actual_figsize = g.fig.get_size_inches() + np.testing.assert_allclose(actual_figsize, custom_figsize) + + with figure_context(): + # Tuple mode: fixed figsize via set_options + with xr.set_options(facetgrid_figsize=(14.0, 5.0)): + g = xplt.FacetGrid(da, col="z") + actual_figsize = g.fig.get_size_inches() + np.testing.assert_allclose(actual_figsize, (14.0, 5.0)) + + with figure_context(): + # Explicit figsize should override the option + with xr.set_options(facetgrid_figsize="rcparams"): + explicit_size = (6.0, 4.0) + g = xplt.FacetGrid(da, col="z", figsize=explicit_size) + actual_figsize = g.fig.get_size_inches() + np.testing.assert_allclose(actual_figsize, explicit_size)