10000 ENH: Support units when specifying the figsize · matplotlib/matplotlib@45454c4 · GitHub
[go: up one dir, main page]

Skip to content

Commit 45454c4

Browse files
committed
ENH: Support units when specifying the figsize
Reviving the spirit of #12402 and #12415, because both had significant user votes on GitHub. This PR is intentionally minimal to only expand the `figsize` parameter when creating a figure. This should be the most relevant use case. Later changing the figure size or reading it is probably less necessary. The minimal approach removes the need to track and return sizes. It is just an enhanced specification capability which directly parses to the internally used inch unit.
1 parent 5defe48 commit 45454c4

File tree

5 files changed

+94
-7
lines changed

5 files changed

+94
-7
lines changed
Lines changed: 9 additions & 0 deletions
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

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

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

lib/matplotlib/figure.pyi

Lines changed: 8 additions & 1 deletion
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 = ...,
@@ -419,3 +421,8 @@ class Figure(FigureBase):
419421
) -> None: ...
420422

421423
def figaspect(arg: float | ArrayLike) -> tuple[float, float]: ...
424+
425+
def _parse_figsize(
426+
figsize: tuple[float, float] | tuple[float, float, Literal["in", "cm", "px"]],
427+
dpi: float
428+
) -> tuple[float, float]: ...

lib/matplotlib/pyplot.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -876,7 +876,9 @@ def figure(
876876
# autoincrement if None, else integer from 1-N
877877
num: int | str | Figure | SubFigure | None = None,
878878
# defaults to rc figure.figsize
879-
figsize: tuple[float, float] | None = None,
879+
figsize: tuple[float, float]
880+
| tuple[float, float, Literal["in", "cm", "px"]]
881+
| None = None,
880882
# defaults to rc figure.dpi
881883
dpi: float | None = None,
882884
*,
@@ -909,8 +911,12 @@ def figure(
909911
window title is set to this value. If num is a ``SubFigure``, its
910912
parent ``Figure`` is activated.
911913
912-
figsize : (float, float), default: :rc:`figure.figsize`
913-
Width, height in inches.
914+
figsize : (float, float) or (float, float, str), default: :rc:`figure.figsize`
915+
The figure dimensions. This can be
916+
917+
- a tuple ``(width, height, unit)``, where *unit* is one of "inch", "cm",
918+
"px".
919+
- a tuple ``(x, y)``, which is interpreted as ``(x, y, "inch")``.
914920
915921
dpi : float, default: :rc:`figure.dpi`
916922
The resolution of the figure in dots-per-inch.

lib/matplotlib/tests/test_figure.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from matplotlib.testing.decorators import image_comparison, check_figures_equal
1717
from matplotlib.axes import Axes
1818
from matplotlib.backend_bases import KeyEvent, MouseEvent
19-
from matplotlib.figure import Figure, FigureBase
19+
from matplotlib.figure import _parse_figsize, Figure, FigureBase
2020
from matplotlib.layout_engine import (ConstrainedLayoutEngine,
2121
TightLayoutEngine,
2222
PlaceHolderLayoutEngine)
@@ -1819,3 +1819,18 @@ def test_subfigure_stale_propagation():
18191819
sfig2.stale = True
18201820
assert sfig1.stale
18211821
assert fig.stale
1822+
1823+
1824+
@pytest.mark.parametrize("input, output", [
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__parse_figsize(input, output):
1831+
assert _parse_figsize(input, dpi=100) == output
1832+
1833+
1834+
def test_invalid_figsize_unit():
1835+
with pytest.raises(ValueError, match="Invalid unit 'um'"):
1836+
plt.figure(figsize=(6, 4, "um"))

0 commit comments

Comments
 (0)
0