Skip to content
2 changes: 1 addition & 1 deletion doc/user-guide/options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``.


Expand Down
5 changes: 5 additions & 0 deletions doc/whats-new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,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 <https://github.com/kkollsga>`_.
- :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
Expand Down
26 changes: 26 additions & 0 deletions xarray/core/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"use_numbagg",
"use_opt_einsum",
"use_flox",
"facetgrid_figsize",
]

class T_Options(TypedDict):
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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"])
Expand Down Expand Up @@ -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)
)
),
}


Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
21 changes: 17 additions & 4 deletions xarray/plot/facetgrid.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,21 @@ def __init__(
else:
raise ValueError("Pass a coordinate name as an argument for row or col")

# exhaust generators
figsize = None if figsize is None else tuple(figsize)
# 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"])
else:
# exhaust generators
figsize = tuple(figsize)

# Compute grid shape
if single_group:
Expand All @@ -239,8 +252,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)

Expand Down
15 changes: 15 additions & 0 deletions xarray/tests/test_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 44 additions & 0 deletions xarray/tests/test_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -3595,3 +3595,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)
Loading