Skip to content

Commit 447d891

Browse files
committed
ENH: Support units when specifying the figsize
Reviving the spirit of matplotlib#12402 and matplotlib#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 447d891

File tree

4 files changed

+72
-7
lines changed

4 files changed

+72
-7
lines changed

lib/matplotlib/figure.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2419,6 +2419,47 @@ def draw(self, renderer):
24192419
self.stale = False
24202420

24212421

2422+
def _parse_figsize(figsize, dpi):
2423+
"""
2424+
Convert a figsize expression to (width, height) in inches.
2425+
2426+
Parameters
2427+
----------
2428+
figsize : (float, float) or (float, float, str)
2429+
This can be
2430+
2431+
- a tuple ``(width, height, unit)``, where *unit* is one of "inch", "cm",
2432+
"px".
2433+
- a tuple ``(x, y)``, which is interpreted as ``(x, y, "inch")``.
2434+
2435+
dpi : float
2436+
The dots-per-inch; used for converting 'px' to 'inch'.
2437+
"""
2438+
num_parts = len(figsize)
2439+
if num_parts == 2:
2440+
return figsize
2441+
elif num_parts == 3:
2442+
x, y, unit = figsize
2443+
if unit == 'inch':
2444+
pass
2445+
elif unit == 'cm':
2446+
x /= 2.54
2447+
y /= 2.54
2448+
elif unit == 'px':
2449+
x /= dpi
2450+
y /= dpi
2451+
else:
2452+
raise ValueError(
2453+
"The unit part of 'figsize' must be one of 'inch', 'cm', 'px', but "
2454+
f"found {unit!r}"
2455+
)
2456+
return x, y
2457+
else:
2458+
raise ValueError(
2459+
"Invalid figsize format, expected (x, y) or (x, y, unit) but got "
2460+
f"{figsize!r}"
2461+
)
2462+
24222463
@_docstring.interpd
24232464
class Figure(FigureBase):
24242465
"""
@@ -2476,8 +2517,12 @@ def __init__(self,
24762517
"""
24772518
Parameters
24782519
----------
2479-
figsize : 2-tuple of floats, default: :rc:`figure.figsize`
2480-
Figure dimension ``(width, height)`` in inches.
2520+
figsize : (float, float) or (float, float, str), default: :rc:`figure.figsize`
2521+
The figure dimensions. This can be
2522+
2523+
- a tuple ``(width, height, unit)``, where *unit* is one of "inch", "cm",
2524+
"px".
2525+
- a tuple ``(x, y)``, which is interpreted as ``(x, y, "inch")``.
24812526
24822527
dpi : float, default: :rc:`figure.dpi`
24832528
Dots per inch.
@@ -2613,6 +2658,8 @@ def __init__(self,
26132658
edgecolor = mpl._val_or_rc(edgecolor, 'figure.edgecolor')
26142659
frameon = mpl._val_or_rc(frameon, 'figure.frameon')
26152660

2661+
figsize = _parse_figsize(figsize, dpi)
2662+
26162663
if not np.isfinite(figsize).all() or (np.array(figsize) < 0).any():
26172664
raise ValueError('figure size must be positive finite not '
26182665
f'{figsize}')

lib/matplotlib/figure.pyi

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,11 @@ class SubFigure(FigureBase):
305305
def axes(self) -> list[Axes]: ... # type: ignore[override]
306306
def get_axes(self) -> list[Axes]: ...
307307

308+
def _parse_figsize(
309+
figsize: tuple[float, float] | tuple[float, float, Literal["inch", "cm", "px"]],
310+
dpi: float
311+
) -> tuple[float, float] = ...
312+
308313
class Figure(FigureBase):
309314
@property
310315
def figure(self) -> Figure: ...
@@ -318,7 +323,7 @@ class Figure(FigureBase):
318323
subplotpars: SubplotParams
319324
def __init__(
320325
self,
321-
figsize: tuple[float, float] | None = ...,
326+
figsize: tuple[float, float] | tuple[float, float, str] | None = ...,
322327
dpi: float | None = ...,
323328
*,
324329
facecolor: ColorType | None = ...,

lib/matplotlib/pyplot.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -876,7 +876,7 @@ 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] | tuple[float, float, Literal["inch", "cm", "px"]] | None = None,
880880
# defaults to rc figure.dpi
881881
dpi: float | None = None,
882882
*,
@@ -909,8 +909,12 @@ def figure(
909909
window title is set to this value. If num is a ``SubFigure``, its
910910
parent ``Figure`` is activated.
911911
912-
figsize : (float, float), default: :rc:`figure.figsize`
913-
Width, height in inches.
912+
figsize : (float, float) or (float, float, str), default: :rc:`figure.figsize`
913+
The figure dimensions. This can be
914+
915+
- a tuple ``(width, height, unit)``, where *unit* is one of "inch", "cm",
916+
"px".
917+
- a tuple ``(x, y)``, which is interpreted as ``(x, y, "inch")``.
914918
915919
dpi : float, default: :rc:`figure.dpi`
916920
The resolution of the figure in dots-per-inch.

lib/matplotlib/tests/test_figure.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,23 @@
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)
2323
from matplotlib.ticker import AutoMinorLocator, FixedFormatter, ScalarFormatter
2424
import matplotlib.pyplot as plt
2525
import matplotlib.dates as mdates
2626

27+
@pytest.mark.parametrize("input, output", [
28+
((6, 4), (6, 4)),
29+
((6, 4, "inch"), (6, 4)),
30+
((5.08, 2.54, "cm"), (2, 1)),
31+
((600, 400, "px"), (6, 4)),
32+
])
33+
def test__parse_figsize(input, output):
34+
assert _parse_figsize(input, dpi=100) == output
35+
2736

2837
@image_comparison(['figure_align_labels'], extensions=['png', 'svg'],
2938
tol=0 if platform.machine() == 'x86_64' else 0.01)

0 commit comments

Comments
 (0)