diff --git a/doc/release/next_whats_new/partial_figsize_none.rst b/doc/release/next_whats_new/partial_figsize_none.rst new file mode 100644 index 000000000000..43bd3e34cc73 --- /dev/null +++ b/doc/release/next_whats_new/partial_figsize_none.rst @@ -0,0 +1,12 @@ +Partial ``figsize`` specification at figure creation +---------------------------------------------------- + +Figure creation now accepts a single ``None`` in ``figsize``. +Passing ``(None, h)`` uses the default width from :rc:`figure.figsize`, and +passing ``(w, None)`` uses the default height. +Passing ``(None, None)`` is invalid and raises a `ValueError`. + +For example:: + + import matplotlib.pyplot as plt + fig = plt.figure(figsize=(None, 4)) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 501b3e32ebf4..ad0206e0db5c 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -2492,6 +2492,9 @@ def __init__(self, - a tuple ``(width, height)``, which is interpreted in inches, i.e. as ``(width, height, "in")``. + One of *width* or *height* may be ``None``; the respective value is + taken from :rc:`figure.figsize`. + dpi : float, default: :rc:`figure.dpi` Dots per inch. @@ -3155,6 +3158,10 @@ def set_size_inches(self, w, h=None, forward=True): """ if h is None: # Got called with a single pair as argument. w, h = w + if w is None or h is None: + raise ValueError( + "Figure.set_size_inches does not accept None; provide both " + "width and height explicitly") size = np.array([w, h]) if not np.isfinite(size).all() or (size < 0).any(): raise ValueError(f'figure size must be positive finite not {size}') @@ -3763,25 +3770,40 @@ def _parse_figsize(figsize, dpi): """ num_parts = len(figsize) if num_parts == 2: - return figsize + x, y = figsize elif num_parts == 3: x, y, unit = figsize if unit == 'in': pass elif unit == 'cm': - x /= 2.54 - y /= 2.54 + if x is not None: + x /= 2.54 + if y is not None: + y /= 2.54 elif unit == 'px': - x /= dpi - y /= dpi + if x is not None: + x /= dpi + if y is not None: + y /= dpi else: raise ValueError( f"Invalid unit {unit!r} in 'figsize'; " "supported units are 'in', 'cm', 'px'" ) - return x, y else: raise ValueError( "Invalid figsize format, expected (x, y) or (x, y, unit) but got " f"{figsize!r}" ) + + if x is None and y is None: + raise ValueError( + "figsize=(None, None) is invalid; at least one of width or " + "height must be provided") + + default_width, default_height = mpl.rcParams["figure.figsize"] + if x is None: + x = default_width + if y is None: + y = default_height + return x, y diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 483bc031336c..ed7821ca1b27 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -947,6 +947,9 @@ def figure( "px". - a tuple ``(x, y)``, which is interpreted as ``(x, y, "inch")``. + One of *width* or *height* may be ``None``; the respective value is taken + from :rc:`figure.figsize`. + dpi : float, default: :rc:`figure.dpi` The resolution of the figure in dots-per-inch. diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index bb358fa6269f..0e793a6e7671 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -1883,6 +1883,24 @@ def test_figsize(figsize, figsize_inches): assert tuple(fig.get_size_inches()) == figsize_inches +def test_figsize_partial_none(): + default_w, default_h = mpl.rcParams["figure.figsize"] + + fig = plt.figure(figsize=(None, 4)) + w, h = fig.get_size_inches() + assert (w, h) == (default_w, 4) + + fig = plt.figure(figsize=(6, None)) + w, h = fig.get_size_inches() + assert (w, h) == (6, default_h) + + +def test_figsize_both_none(): + with pytest.raises(ValueError, + match=r"figsize=\(None, None\) is invalid"): + plt.figure(figsize=(None, None)) + + def test_figsize_invalid_unit(): with pytest.raises(ValueError, match="Invalid unit 'um'"): plt.figure(figsize=(6, 4, "um"))