Skip to content

Commit 186df24

Browse files
committed
ENH: add new layout engine for tight_layout and constrained_layout
1 parent d001ed1 commit 186df24

File tree

9 files changed

+261
-132
lines changed

9 files changed

+261
-132
lines changed

lib/matplotlib/_constrained_layout.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from matplotlib import _api
2121
import matplotlib.transforms as mtransforms
2222
import matplotlib._layoutgrid as layoutgrid
23-
23+
from matplotlib.backend_bases import _get_renderer
2424

2525
_log = logging.getLogger(__name__)
2626

@@ -62,7 +62,7 @@
6262

6363

6464
######################################################
65-
def do_constrained_layout(fig, renderer, h_pad, w_pad,
65+
def do_constrained_layout(fig, h_pad, w_pad,
6666
hspace=None, wspace=None):
6767
"""
6868
Do the constrained_layout. Called at draw time in
@@ -92,6 +92,7 @@ def do_constrained_layout(fig, renderer, h_pad, w_pad,
9292
layoutgrid: useful for debugging.
9393
"""
9494

95+
renderer = _get_renderer(fig)
9596
# make layoutgrid tree...
9697
_layoutgrids = _make_layoutgrids(fig, None)
9798
if not _layoutgrids['hasgrids']:

lib/matplotlib/_layoutgrid.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,7 @@ def plot_children(fig, lg=None, level=0, printit=False):
513513
import matplotlib.patches as mpatches
514514

515515
if lg is None:
516-
_layoutgrids = fig.execute_constrained_layout()
516+
_layoutgrids = fig.layout_engine.execute()
517517
lg = _layoutgrids[fig]
518518
colors = plt.rcParams["axes.prop_cycle"].by_key()["color"]
519519
col = colors[level]

lib/matplotlib/backend_bases.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2259,7 +2259,7 @@ def print_figure(
22592259
if bbox_inches is None:
22602260
bbox_inches = rcParams['savefig.bbox']
22612261

2262-
if (self.figure.get_constrained_layout() or
2262+
if (self.figure.layout_engine is not None or
22632263
bbox_inches == "tight"):
22642264
# we need to trigger a draw before printing to make sure
22652265
# CL works. "tight" also needs a draw to get the right
@@ -2288,8 +2288,8 @@ def print_figure(
22882288
else:
22892289
_bbox_inches_restore = None
22902290

2291-
# we have already done CL above, so turn it off:
2292-
stack.enter_context(self.figure._cm_set(constrained_layout=False))
2291+
# we have already layout above, so turn it off:
2292+
stack.enter_context(self.figure._cm_set(layout_engine=None))
22932293
try:
22942294
# _get_renderer may change the figure dpi (as vector formats
22952295
# force the figure dpi to 72), so we need to set it again here.

lib/matplotlib/figure.py

Lines changed: 91 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
from matplotlib.text import Text
4040
from matplotlib.transforms import (Affine2D, Bbox, BboxTransformTo,
4141
TransformedBbox)
42+
from matplotlib.layout_engine import (constrained_layout_engine,
43+
tight_layout_engine, LayoutEngine)
4244

4345
_log = logging.getLogger(__name__)
4446

@@ -1144,12 +1146,15 @@ def colorbar(self, mappable, cax=None, ax=None, use_gridspec=True, **kw):
11441146
"silence this warning, explicitly pass the 'ax' argument "
11451147
"to colorbar().")
11461148

1149+
if use_gridspec:
1150+
if (self.layout_engine is not None and
1151+
not self.layout_engine.colorbar_gridspec):
1152+
use_gridspec = False
11471153
# Store the value of gca so that we can set it back later on.
11481154
if cax is None:
11491155
current_ax = self.gca()
11501156
kw['userax'] = False
1151-
if (use_gridspec and isinstance(ax, SubplotBase)
1152-
and not self.get_constrained_layout()):
1157+
if (use_gridspec and isinstance(ax, SubplotBase)):
11531158
cax, kw = cbar.make_axes_gridspec(ax, **kw)
11541159
else:
11551160
cax, kw = cbar.make_axes(ax, **kw)
@@ -1197,12 +1202,13 @@ def subplots_adjust(self, left=None, bottom=None, right=None, top=None,
11971202
The height of the padding between subplots,
11981203
as a fraction of the average Axes height.
11991204
"""
1200-
if self.get_constrained_layout():
1201-
self.set_constrained_layout(False)
1205+
if (self.layout_engine and
1206+
not self.layout_engine.adjust_compatible):
12021207
_api.warn_external(
1203-
"This figure was using constrained_layout, but that is "
1208+
"This figure was using a layout engine that is "
12041209
"incompatible with subplots_adjust and/or tight_layout; "
1205-
"disabling constrained_layout.")
1210+
"not calling subplots_adjust")
1211+
return
12061212
self.subplotpars.update(left, bottom, right, top, wspace, hspace)
12071213
for ax in self.axes:
12081214
if isinstance(ax, SubplotBase):
@@ -2207,6 +2213,7 @@ def __init__(self,
22072213
"""
22082214
super().__init__()
22092215

2216+
self.layout_engine = None
22102217
if layout is not None:
22112218
if tight_layout is not None:
22122219
_api.warn_external(
@@ -2216,14 +2223,15 @@ def __init__(self,
22162223
_api.warn_external(
22172224
"The Figure parameters 'layout' and 'constrained_layout' "
22182225
"cannot be used together. Please use 'layout' only.")
2219-
if layout == 'constrained':
2220-
tight_layout = False
2221-
constrained_layout = True
2222-
elif layout == 'tight':
2223-
tight_layout = True
2224-
constrained_layout = False
2225-
else:
2226-
raise ValueError(f"Invalid value for 'layout': {layout!r}")
2226+
self.set_layout_engine(layout)
2227+
elif tight_layout is not None:
2228+
self.set_layout_engine(layout='tight')
2229+
if isinstance(tight_layout, dict):
2230+
self.get_layout_engine().set_info(tight_layout)
2231+
elif constrained_layout is not None:
2232+
self.set_layout_engine(layout='constrained')
2233+
if isinstance(constrained_layout, dict):
2234+
self.get_layout_engine().set_info(constrained_layout)
22272235

22282236
self.callbacks = cbook.CallbackRegistry()
22292237
# Callbacks traditionally associated with the canvas (and exposed with
@@ -2274,20 +2282,37 @@ def __init__(self,
22742282

22752283
self.subplotpars = subplotpars
22762284

2277-
# constrained_layout:
2278-
self._constrained = False
2279-
2280-
self.set_tight_layout(tight_layout)
2281-
22822285
self._axstack = _AxesStack() # track all figure axes and current axes
22832286
self.clf()
22842287
self._cachedRenderer = None
22852288

2286-
self.set_constrained_layout(constrained_layout)
2287-
22882289
# list of child gridspecs for this figure
22892290
self._gridspecs = []
22902291

2292+
def set_layout_engine(self, layout=None, **kwargs):
2293+
"""
2294+
Set the layout engine... FIXME
2295+
"""
2296+
if layout is None:
2297+
if mpl.rcParams['figure.autolayout']:
2298+
layout = 'tight'
2299+
elif mpl.rcParams['figure.constrained_layout.use']:
2300+
layout = 'constrained'
2301+
else:
2302+
self.layout_engine = None
2303+
return
2304+
if layout == 'tight':
2305+
self.layout_engine = tight_layout_engine(self, **kwargs)
2306+
elif layout == 'constrained':
2307+
self.layout_engine = constrained_layout_engine(self, **kwargs)
2308+
elif isinstance(layout, LayoutEngine):
2309+
self.layout_engine = layout
2310+
else:
2311+
raise ValueError(f"Invalid value for 'layout': {layout!r}")
2312+
2313+
def get_layout_engine(self):
2314+
return self.layout_engine
2315+
22912316
# TODO: I'd like to dynamically add the _repr_html_ method
22922317
# to the figure in the right context, but then IPython doesn't
22932318
# use it, for some reason.
@@ -2377,8 +2402,10 @@ def _set_dpi(self, dpi, forward=True):
23772402

23782403
def get_tight_layout(self):
23792404
"""Return whether `.tight_layout` is called when drawing."""
2380-
return self._tight
2405+
return isinstance(self.layout_engine, tight_layout_engine)
23812406

2407+
@_api.deprecated(
2408+
"3.5", alternative="set_layout_engine")
23822409
def set_tight_layout(self, tight):
23832410
"""
23842411
Set whether and how `.tight_layout` is called when drawing.
@@ -2393,8 +2420,9 @@ def set_tight_layout(self, tight):
23932420
"""
23942421
if tight is None:
23952422
tight = mpl.rcParams['figure.autolayout']
2396-
self._tight = bool(tight)
2397-
self._tight_parameters = tight if isinstance(tight, dict) else {}
2423+
_tight_parameters = tight if isinstance(tight, dict) else {}
2424+
if bool(tight):
2425+
self.layout_engine = tight_layout_engine(self, _tight_parameters)
23982426
self.stale = True
23992427

24002428
def get_constrained_layout(self):
@@ -2403,8 +2431,10 @@ def get_constrained_layout(self):
24032431
24042432
See :doc:`/tutorials/intermediate/constrainedlayout_guide`.
24052433
"""
2406-
return self._constrained
2434+
return isinstance(self.layout_engine, constrained_layout_engine)
24072435

2436+
@_api.deprecated(
2437+
"3.5", alternative="set_layout_engine('constrained')")
24082438
def set_constrained_layout(self, constrained):
24092439
"""
24102440
Set whether ``constrained_layout`` is used upon drawing. If None,
@@ -2421,20 +2451,16 @@ def set_constrained_layout(self, constrained):
24212451
----------
24222452
constrained : bool or dict or None
24232453
"""
2424-
self._constrained_layout_pads = dict()
2425-
self._constrained_layout_pads['w_pad'] = None
2426-
self._constrained_layout_pads['h_pad'] = None
2427-
self._constrained_layout_pads['wspace'] = None
2428-
self._constrained_layout_pads['hspace'] = None
24292454
if constrained is None:
24302455
constrained = mpl.rcParams['figure.constrained_layout.use']
2431-
self._constrained = bool(constrained)
2432-
if isinstance(constrained, dict):
2433-
self.set_constrained_layout_pads(**constrained)
2434-
else:
2435-
self.set_constrained_layout_pads()
2456+
_constrained = bool(constrained)
2457+
_parameters = constrained if isinstance(constrained, dict) else {}
2458+
if _constrained:
2459+
self.layout_engine = constrained_layout_engine(self, _parameters)
24362460
self.stale = True
24372461

2462+
@_api.deprecated(
2463+
"3.5", alternative="figure.layout_engine.set_info()")
24382464
def set_constrained_layout_pads(self, **kwargs):
24392465
"""
24402466
Set padding for ``constrained_layout``. Note the kwargs can be passed
@@ -2461,18 +2487,15 @@ def set_constrained_layout_pads(self, **kwargs):
24612487
subplot width. The total padding ends up being h_pad + hspace.
24622488
24632489
"""
2490+
if isinstance(self.layout_engine, constrained_layout_engine):
2491+
self.layout_engine.set_info(**kwargs)
24642492

2465-
todo = ['w_pad', 'h_pad', 'wspace', 'hspace']
2466-
for td in todo:
2467-
if td in kwargs and kwargs[td] is not None:
2468-
self._constrained_layout_pads[td] = kwargs[td]
2469-
else:
2470-
self._constrained_layout_pads[td] = (
2471-
mpl.rcParams['figure.constrained_layout.' + td])
2472-
2493+
@_api.deprecated(
2494+
"3.5", alternative="fig.layout_engine.get_info()")
24732495
def get_constrained_layout_pads(self, relative=False):
24742496
"""
2475-
Get padding for ``constrained_layout``.
2497+
Get padding for ``constrained_layout`` if it is the
2498+
``layout_engine``.
24762499
24772500
Returns a list of ``w_pad, h_pad`` in inches and
24782501
``wspace`` and ``hspace`` as fractions of the subplot.
@@ -2484,10 +2507,14 @@ def get_constrained_layout_pads(self, relative=False):
24842507
relative : bool
24852508
If `True`, then convert from inches to figure relative.
24862509
"""
2487-
w_pad = self._constrained_layout_pads['w_pad']
2488-
h_pad = self._constrained_layout_pads['h_pad']
2489-
wspace = self._constrained_layout_pads['wspace']
2490-
hspace = self._constrained_layout_pads['hspace']
2510+
2511+
if not isinstance(self.layout_engine, constrained_layout_engine):
2512+
return None, None, None, None
2513+
info = self.layout_engine.get_info()
2514+
w_pad = info['w_pad']
2515+
h_pad = info['h_pad']
2516+
wspace = info['wspace']
2517+
hspace = info['hspace']
24912518

24922519
if relative and (w_pad is not None or h_pad is not None):
24932520
renderer = _get_renderer(self)
@@ -2765,14 +2792,12 @@ def draw(self, renderer):
27652792

27662793
try:
27672794
renderer.open_group('figure', gid=self.get_gid())
2768-
if self.get_constrained_layout() and self.axes:
2769-
self.execute_constrained_layout(renderer)
2770-
if self.get_tight_layout() and self.axes:
2795+
if self.axes and self.layout_engine is not None:
27712796
try:
2772-
self.tight_layout(**self._tight_parameters)
2797+
pass
2798+
self.layout_engine.execute()
27732799
except ValueError:
27742800
pass
2775-
# ValueError can occur when resizing a window.
27762801

27772802
self.patch.draw(renderer)
27782803
mimage._draw_list_compositing_images(
@@ -3102,6 +3127,8 @@ def handler(ev):
31023127

31033128
return None if event is None else event.name == "key_press_event"
31043129

3130+
@_api.deprecated(
3131+
"3.5", alternative="figure.layout_engine.execute()")
31053132
def execute_constrained_layout(self, renderer=None):
31063133
"""
31073134
Use ``layoutgrid`` to determine pos positions within Axes.
@@ -3112,22 +3139,12 @@ def execute_constrained_layout(self, renderer=None):
31123139
-------
31133140
layoutgrid : private debugging object
31143141
"""
3142+
if not isinstance(self.layout_engine, constrained_layout_engine):
3143+
return None
31153144

3116-
from matplotlib._constrained_layout import do_constrained_layout
3117-
3118-
_log.debug('Executing constrainedlayout')
3119-
w_pad, h_pad, wspace, hspace = self.get_constrained_layout_pads()
3120-
# convert to unit-relative lengths
3121-
fig = self
3122-
width, height = fig.get_size_inches()
3123-
w_pad = w_pad / width
3124-
h_pad = h_pad / height
3125-
if renderer is None:
3126-
renderer = _get_renderer(fig)
3127-
return do_constrained_layout(fig, renderer, h_pad, w_pad,
3128-
hspace, wspace)
3145+
return self.layout_engine.execute()
31293146

3130-
def tight_layout(self, *, pad=1.08, h_pad=None, w_pad=None, rect=None):
3147+
def tight_layout(self, **kwargs):
31313148
"""
31323149
Adjust the padding between and around subplots.
31333150
@@ -3149,25 +3166,20 @@ def tight_layout(self, *, pad=1.08, h_pad=None, w_pad=None, rect=None):
31493166
31503167
See Also
31513168
--------
3152-
.Figure.set_tight_layout
3169+
.Figure.set_layout_engine
31533170
.pyplot.tight_layout
31543171
"""
3155-
from contextlib import nullcontext
3156-
from .tight_layout import (
3157-
get_subplotspec_list, get_tight_layout_figure)
3172+
from .tight_layout import get_subplotspec_list
3173+
31583174
subplotspec_list = get_subplotspec_list(self.axes)
31593175
if None in subplotspec_list:
31603176
_api.warn_external("This figure includes Axes that are not "
31613177
"compatible with tight_layout, so results "
31623178
"might be incorrect.")
3163-
renderer = _get_renderer(self)
3164-
with getattr(renderer, "_draw_disabled", nullcontext)():
3165-
kwargs = get_tight_layout_figure(
3166-
self, self.axes, subplotspec_list, renderer,
3167-
pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect)
3168-
if kwargs:
3169-
self.subplots_adjust(**kwargs)
3170-
3179+
# note that here we do not _set_ the figures engine to tight_layout
3180+
# but rather just perform the layout in place for back compatibility.
3181+
engine = tight_layout_engine(fig=self, **kwargs)
3182+
engine.execute()
31713183

31723184
def figaspect(arg):
31733185
"""

0 commit comments

Comments
 (0)