From 5160ea8f2a8aefc8eda3de702cd8799d948167f4 Mon Sep 17 00:00:00 2001 From: kkollsga Date: Wed, 11 Feb 2026 00:46:49 +0100 Subject: [PATCH 1/4] Add facetgrid_figsize option to set_options (#11103) Add a new `facetgrid_figsize` option to `xr.set_options()` that controls how FacetGrid determines figure size when `figsize` is not explicitly passed. When set to `"rcparams"`, FacetGrid uses `matplotlib.rcParams['figure.figsize']` instead of computing size from `size` and `aspect`. Default is `"computed"` (current behavior). Co-authored-by: Claude --- doc/whats-new.rst | 4 ++++ xarray/core/options.py | 15 +++++++++++++++ xarray/plot/facetgrid.py | 15 +++++++++++---- xarray/tests/test_options.py | 9 +++++++++ xarray/tests/test_plot.py | 37 ++++++++++++++++++++++++++++++++++++ 5 files changed, 76 insertions(+), 4 deletions(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 41679520930..41ec084b758 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -155,6 +155,10 @@ 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']`` + 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..a959760beed 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"] 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,7 @@ 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": _FACETGRID_FIGSIZE_OPTIONS.__contains__, } @@ -222,6 +227,14 @@ 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"}, 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. 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 +370,8 @@ 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}" 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..16fc6bddec2 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -212,10 +212,17 @@ 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 - cbar_space = 1 - figsize = (ncol * size * aspect + cbar_space, nrow * size) + from xarray.core.options import OPTIONS + + if OPTIONS["facetgrid_figsize"] == "rcparams": + import matplotlib as mpl + + figsize = tuple(mpl.rcParams["figure.figsize"]) + else: + # Calculate the base figure size with extra horizontal space + # for a colorbar + cbar_space = 1 + figsize = (ncol * size * aspect + cbar_space, nrow * size) fig, axs = plt.subplots( nrow, diff --git a/xarray/tests/test_options.py b/xarray/tests/test_options.py index ca9c1fd6440..99bb8901f94 100644 --- a/xarray/tests/test_options.py +++ b/xarray/tests/test_options.py @@ -84,6 +84,15 @@ 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 xarray.set_options(facetgrid_figsize="rcparams"): + assert OPTIONS["facetgrid_figsize"] == "rcparams" + with xarray.set_options(facetgrid_figsize="computed"): + assert OPTIONS["facetgrid_figsize"] == "computed" + + 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..d50f8fe0a15 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -3567,3 +3567,40 @@ 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(): + # 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) From 6b1a002668e5d281e7a6b7cdbcf3f8c4dace8f1b Mon Sep 17 00:00:00 2001 From: kkollsga Date: Sun, 29 Mar 2026 11:39:26 +0200 Subject: [PATCH 2/4] Support tuple figsize in facetgrid_figsize option (#11103) Extend facetgrid_figsize to accept a (width, height) tuple in addition to "computed" and "rcparams". Also move figsize resolution before grid shape computation for better composition with col_wrap="auto" (#11266). Co-authored-by: Claude --- doc/whats-new.rst | 3 ++- xarray/core/options.py | 16 ++++++++++++---- xarray/plot/facetgrid.py | 28 +++++++++++++++++----------- xarray/tests/test_options.py | 6 ++++++ xarray/tests/test_plot.py | 7 +++++++ 5 files changed, 44 insertions(+), 16 deletions(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 41ec084b758..6e65861b904 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -157,7 +157,8 @@ New Features - Added ``facetgrid_figsize`` option to :py:func:`~xarray.set_options` allowing :py:class:`~xarray.plot.FacetGrid` to use ``matplotlib.rcParams['figure.figsize']`` - instead of computing figure size from ``size`` and ``aspect`` (:issue:`11103`). + 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 diff --git a/xarray/core/options.py b/xarray/core/options.py index a959760beed..f257bb553cf 100644 --- a/xarray/core/options.py +++ b/xarray/core/options.py @@ -74,7 +74,7 @@ class T_Options(TypedDict): use_new_combine_kwarg_defaults: bool use_numbagg: bool use_opt_einsum: bool - facetgrid_figsize: Literal["computed", "rcparams"] + facetgrid_figsize: Literal["computed", "rcparams"] | tuple[float, float] OPTIONS: T_Options = { @@ -148,7 +148,11 @@ 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": _FACETGRID_FIGSIZE_OPTIONS.__contains__, + "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) + ), } @@ -227,7 +231,7 @@ 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"}, default: "computed" + facetgrid_figsize : {"computed", "rcparams"} or tuple of float, default: "computed" How :class:`~xarray.plot.FacetGrid` determines figure size when ``figsize`` is not explicitly passed: @@ -235,6 +239,7 @@ class set_options: 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 @@ -371,7 +376,10 @@ def __init__(self, **kwargs): 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}" + 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 16fc6bddec2..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,17 +225,10 @@ def __init__( subplot_kws = {} if subplot_kws is None else subplot_kws if figsize is None: - from xarray.core.options import OPTIONS - - if OPTIONS["facetgrid_figsize"] == "rcparams": - import matplotlib as mpl - - figsize = tuple(mpl.rcParams["figure.figsize"]) - else: - # Calculate the base figure size with extra horizontal space - # for a colorbar - cbar_space = 1 - figsize = (ncol * size * aspect + cbar_space, nrow * size) + # Calculate the base figure size with extra horizontal space + # for a colorbar + cbar_space = 1 + figsize = (ncol * size * aspect + cbar_space, nrow * size) fig, axs = plt.subplots( nrow, diff --git a/xarray/tests/test_options.py b/xarray/tests/test_options.py index 99bb8901f94..fd05f9d5122 100644 --- a/xarray/tests/test_options.py +++ b/xarray/tests/test_options.py @@ -87,10 +87,16 @@ def test_netcdf_engine_order() -> None: 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: diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index d50f8fe0a15..b6805ec3957 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -3597,6 +3597,13 @@ def test_facetgrid_figsize_rcparams() -> None: 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"): From 9e280448946a6cc993fc38bbbf57a3a5769ebae6 Mon Sep 17 00:00:00 2001 From: kkollsga Date: Sun, 29 Mar 2026 16:52:16 +0200 Subject: [PATCH 3/4] Update options.rst: broaden plotting section, add facetgrid_figsize Co-authored-by: Claude --- doc/user-guide/options.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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``. From be8431ce327efe306fadab2f7ca37953a3c6d051 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 29 Mar 2026 14:52:47 +0000 Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- xarray/core/options.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/xarray/core/options.py b/xarray/core/options.py index f257bb553cf..b43e4ff3b09 100644 --- a/xarray/core/options.py +++ b/xarray/core/options.py @@ -148,10 +148,13 @@ 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) + "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) + ) ), }