Skip to content

ENH: Support units when specifying the figsize #29612

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions doc/users/next_whats_new/figsize_unit.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Figure size units
-----------------

When creating figures, it is now possible to define figure sizes in cm or pixel.

Up to now the figure size is specified via ``plt.figure(..., figsize=(6, 4))``,
and the given numbers are interpreted as inches. It is now possible to add a
unit string to the tuple, i.e. ``plt.figure(..., figsize=(600, 400, "px"))``.
Supported unit strings are "in", "cm", "px".
54 changes: 52 additions & 2 deletions lib/matplotlib/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -2475,8 +2475,13 @@
"""
Parameters
----------
figsize : 2-tuple of floats, default: :rc:`figure.figsize`
Figure dimension ``(width, height)`` in inches.
figsize : (float, float) or (float, float, str), default: :rc:`figure.figsize`
The figure dimensions. This can be

- a tuple ``(width, height, unit)``, where *unit* is one of "in" (inch),
"cm" (centimenter), "px" (pixel).
- a tuple ``(width, height)``, which is interpreted in inches, i.e. as
``(width, height, "in")``.

dpi : float, default: :rc:`figure.dpi`
Dots per inch.
Expand Down Expand Up @@ -2612,6 +2617,8 @@
edgecolor = mpl._val_or_rc(edgecolor, 'figure.edgecolor')
frameon = mpl._val_or_rc(frameon, 'figure.frameon')

figsize = _parse_figsize(figsize, dpi)

if not np.isfinite(figsize).all() or (np.array(figsize) < 0).any():
raise ValueError('figure size must be positive finite not '
f'{figsize}')
Expand Down Expand Up @@ -3713,3 +3720,46 @@
# the min/max dimensions (we don't want figures 10 feet tall!)
newsize = np.clip(newsize, figsize_min, figsize_max)
return newsize


def _parse_figsize(figsize, dpi):
"""
Convert a figsize expression to (width, height) in inches.

Parameters
----------
figsize : (float, float) or (float, float, str)
This can be

- a tuple ``(width, height, unit)``, where *unit* is one of "in" (inch),
"cm" (centimenter), "px" (pixel).
- a tuple ``(width, height)``, which is interpreted in inches, i.e. as
``(width, height, "in")``.

dpi : float
The dots-per-inch; used for converting 'px' to 'in'.
"""
num_parts = len(figsize)
if num_parts == 2:
return figsize
elif num_parts == 3:
x, y, unit = figsize
if unit == 'in':
pass
elif unit == 'cm':
x /= 2.54
y /= 2.54
elif unit == 'px':
x /= dpi
y /= dpi
else:
raise ValueError(
f"Invalid unit {unit!r} in 'figsize'; "
"supported units are 'in', 'cm', 'px'"
)
return x, y
else:
raise ValueError(

Check warning on line 3762 in lib/matplotlib/figure.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/figure.py#L3762

Added line #L3762 was not covered by tests
"Invalid figsize format, expected (x, y) or (x, y, unit) but got "
f"{figsize!r}"
)
9 changes: 8 additions & 1 deletion lib/matplotlib/figure.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,9 @@ class Figure(FigureBase):
subplotpars: SubplotParams
def __init__(
self,
figsize: tuple[float, float] | None = ...,
figsize: tuple[float, float]
| tuple[float, float, Literal["in", "cm", "px"]]
| None = ...,
dpi: float | None = ...,
*,
facecolor: ColorType | None = ...,
Expand Down Expand Up @@ -421,3 +423,8 @@ class Figure(FigureBase):
def figaspect(
arg: float | ArrayLike,
) -> np.ndarray[tuple[Literal[2]], np.dtype[np.float64]]: ...

def _parse_figsize(
figsize: tuple[float, float] | tuple[float, float, Literal["in", "cm", "px"]],
dpi: float
) -> tuple[float, float]: ...
12 changes: 9 additions & 3 deletions lib/matplotlib/pyplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -875,7 +875,9 @@ def figure(
# autoincrement if None, else integer from 1-N
num: int | str | Figure | SubFigure | None = None,
# defaults to rc figure.figsize
figsize: ArrayLike | None = None,
figsize: ArrayLike # a 2-element ndarray is accepted as well
| tuple[float, float, Literal["in", "cm", "px"]]
| None = None,
# defaults to rc figure.dpi
dpi: float | None = None,
*,
Expand Down Expand Up @@ -908,8 +910,12 @@ def figure(
window title is set to this value. If num is a ``SubFigure``, its
parent ``Figure`` is activated.

figsize : (float, float), default: :rc:`figure.figsize`
Width, height in inches.
figsize : (float, float) or (float, float, str), default: :rc:`figure.figsize`
The figure dimensions. This can be

- a tuple ``(width, height, unit)``, where *unit* is one of "inch", "cm",
"px".
- a tuple ``(x, y)``, which is interpreted as ``(x, y, "inch")``.

dpi : float, default: :rc:`figure.dpi`
The resolution of the figure in dots-per-inch.
Expand Down
16 changes: 16 additions & 0 deletions lib/matplotlib/tests/test_figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -1819,3 +1819,19 @@ def test_subfigure_stale_propagation():
sfig2.stale = True
assert sfig1.stale
assert fig.stale


@pytest.mark.parametrize("figsize, figsize_inches", [
((6, 4), (6, 4)),
((6, 4, "in"), (6, 4)),
((5.08, 2.54, "cm"), (2, 1)),
((600, 400, "px"), (6, 4)),
])
def test_figsize(figsize, figsize_inches):
fig = plt.figure(figsize=figsize, dpi=100)
assert tuple(fig.get_size_inches()) == figsize_inches


def test_figsize_invalid_unit():
with pytest.raises(ValueError, match="Invalid unit 'um'"):
plt.figure(figsize=(6, 4, "um"))
Loading