Skip to content

Commit 4fa258a

Browse files
authored
Merge pull request matplotlib#29612 from timhoffm/figsize-unit
ENH: Support units when specifying the figsize
2 parents 33dbc47 + d443e9d commit 4fa258a

File tree

5 files changed

+94
-6
lines changed

5 files changed

+94
-6
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Figure size units
2+
-----------------
3+
4+
When creating figures, it is now possible to define figure sizes in cm or pixel.
5+
6+
Up to now the figure size is specified via ``plt.figure(..., figsize=(6, 4))``,
7+
and the given numbers are interpreted as inches. It is now possible to add a
8+
unit string to the tuple, i.e. ``plt.figure(..., figsize=(600, 400, "px"))``.
9+
Supported unit strings are "in", "cm", "px".

lib/matplotlib/figure.py

+52-2
Original file line numberDiff line numberDiff line change
@@ -2475,8 +2475,13 @@ def __init__(self,
24752475
"""
24762476
Parameters
24772477
----------
2478-
figsize : 2-tuple of floats, default: :rc:`figure.figsize`
2479-
Figure dimension ``(width, height)`` in inches.
2478+
figsize : (float, float) or (float, float, str), default: :rc:`figure.figsize`
2479+
The figure dimensions. This can be
2480+
2481+
- a tuple ``(width, height, unit)``, where *unit* is one of "in" (inch),
2482+
"cm" (centimenter), "px" (pixel).
2483+
- a tuple ``(width, height)``, which is interpreted in inches, i.e. as
2484+
``(width, height, "in")``.
24802485
24812486
dpi : float, default: :rc:`figure.dpi`
24822487
Dots per inch.
@@ -2612,6 +2617,8 @@ def __init__(self,
26122617
edgecolor = mpl._val_or_rc(edgecolor, 'figure.edgecolor')
26132618
frameon = mpl._val_or_rc(frameon, 'figure.frameon')
26142619

2620+
figsize = _parse_figsize(figsize, dpi)
2621+
26152622
if not np.isfinite(figsize).all() or (np.array(figsize) < 0).any():
26162623
raise ValueError('figure size must be positive finite not '
26172624
f'{figsize}')
@@ -3713,3 +3720,46 @@ def figaspect(arg):
37133720
# the min/max dimensions (we don't want figures 10 feet tall!)
37143721
newsize = np.clip(newsize, figsize_min, figsize_max)
37153722
return newsize
3723+
3724+
3725+
def _parse_figsize(figsize, dpi):
3726+
"""
3727+
Convert a figsize expression to (width, height) in inches.
3728+
3729+
Parameters
3730+
----------
3731+
figsize : (float, float) or (float, float, str)
3732+
This can be
3733+
3734+
- a tuple ``(width, height, unit)``, where *unit* is one of "in" (inch),
3735+
"cm" (centimenter), "px" (pixel).
3736+
- a tuple ``(width, height)``, which is interpreted in inches, i.e. as
3737+
``(width, height, "in")``.
3738+
3739+
dpi : float
3740+
The dots-per-inch; used for converting 'px' to 'in'.
3741+
"""
3742+
num_parts = len(figsize)
3743+
if num_parts == 2:
3744+
return figsize
3745+
elif num_parts == 3:
3746+
x, y, unit = figsize
3747+
if unit == 'in':
3748+
pass
3749+
elif unit == 'cm':
3750+
x /= 2.54
3751+
y /= 2.54
3752+
elif unit == 'px':
3753+
x /= dpi
3754+
y /= dpi
3755+
else:
3756+
raise ValueError(
3757+
f"Invalid unit {unit!r} in 'figsize'; "
3758+
"supported units are 'in', 'cm', 'px'"
3759+
)
3760+
return x, y
3761+
else:
3762+
raise ValueError(
3763+
"Invalid figsize format, expected (x, y) or (x, y, unit) but got "
3764+
f"{figsize!r}"
3765+
)

lib/matplotlib/figure.pyi

+8-1
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,9 @@ class Figure(FigureBase):
318318
subplotpars: SubplotParams
319319
def __init__(
320320
self,
321-
figsize: tuple[float, float] | None = ...,
321+
figsize: tuple[float, float]
322+
| tuple[float, float, Literal["in", "cm", "px"]]
323+
| None = ...,
322324
dpi: float | None = ...,
323325
*,
324326
facecolor: ColorType | None = ...,
@@ -421,3 +423,8 @@ class Figure(FigureBase):
421423
def figaspect(
422424
arg: float | ArrayLike,
423425
) -> np.ndarray[tuple[Literal[2]], np.dtype[np.float64]]: ...
426+
427+
def _parse_figsize(
428+
figsize: tuple[float, float] | tuple[float, float, Literal["in", "cm", "px"]],
429+
dpi: float
430+
) -> tuple[float, float]: ...

lib/matplotlib/pyplot.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -875,7 +875,9 @@ def figure(
875875
# autoincrement if None, else integer from 1-N
876876
num: int | str | Figure | SubFigure | None = None,
877877
# defaults to rc figure.figsize
878-
figsize: ArrayLike | None = None,
878+
figsize: ArrayLike # a 2-element ndarray is accepted as well
879+
| tuple[float, float, Literal["in", "cm", "px"]]
880+
| None = None,
879881
# defaults to rc figure.dpi
880882
dpi: float | None = None,
881883
*,
@@ -908,8 +910,12 @@ def figure(
908910
window title is set to this value. If num is a ``SubFigure``, its
909911
parent ``Figure`` is activated.
910912
911-
figsize : (float, float), default: :rc:`figure.figsize`
912-
Width, height in inches.
913+
figsize : (float, float) or (float, float, str), default: :rc:`figure.figsize`
914+
The figure dimensions. This can be
915+
916+
- a tuple ``(width, height, unit)``, where *unit* is one of "inch", "cm",
917+
"px".
918+
- a tuple ``(x, y)``, which is interpreted as ``(x, y, "inch")``.
913919
914920
dpi : float, default: :rc:`figure.dpi`
915921
The resolution of the figure in dots-per-inch.

lib/matplotlib/tests/test_figure.py

+16
Original file line numberDiff line numberDiff line change
@@ -1819,3 +1819,19 @@ def test_subfigure_stale_propagation():
18191819
sfig2.stale = True
18201820
assert sfig1.stale
18211821
assert fig.stale
1822+
1823+
1824+
@pytest.mark.parametrize("figsize, figsize_inches", [
1825+
((6, 4), (6, 4)),
1826+
((6, 4, "in"), (6, 4)),
1827+
((5.08, 2.54, "cm"), (2, 1)),
1828+
((600, 400, "px"), (6, 4)),
1829+
])
1830+
def test_figsize(figsize, figsize_inches):
1831+
fig = plt.figure(figsize=figsize, dpi=100)
1832+
assert tuple(fig.get_size_inches()) == figsize_inches
1833+
1834+
1835+
def test_figsize_invalid_unit():
1836+
with pytest.raises(ValueError, match="Invalid unit 'um'"):
1837+
plt.figure(figsize=(6, 4, "um"))

0 commit comments

Comments
 (0)