Skip to content

Commit 544d829

Browse files
committed
ENH: implement and use base layout_engine for more flexible layout.
1 parent 0666c59 commit 544d829

11 files changed

+328
-193
lines changed

lib/matplotlib/_constrained_layout.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import numpy as np
1919

2020
from matplotlib import _api, artist as martist
21+
from matplotlib.backend_bases import _get_renderer
2122
import matplotlib.transforms as mtransforms
2223
import matplotlib._layoutgrid as mlayoutgrid
2324

@@ -62,7 +63,7 @@
6263

6364

6465
######################################################
65-
def do_constrained_layout(fig, renderer, h_pad, w_pad,
66+
def do_constrained_layout(fig, h_pad, w_pad,
6667
hspace=None, wspace=None):
6768
"""
6869
Do the constrained_layout. Called at draw time in
@@ -91,6 +92,7 @@ def do_constrained_layout(fig, renderer, h_pad, w_pad,
9192
layoutgrid : private debugging structure
9293
"""
9394

95+
renderer = _get_renderer(fig)
9496
# make layoutgrid tree...
9597
layoutgrids = make_layoutgrids(fig, None)
9698
if not layoutgrids['hasgrids']:

lib/matplotlib/backend_bases.py

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

2275-
if (self.figure.get_constrained_layout() or
2275+
if (self.figure.layout_engine is not None or
22762276
bbox_inches == "tight"):
22772277
# we need to trigger a draw before printing to make sure
22782278
# CL works. "tight" also needs a draw to get the right
@@ -2301,8 +2301,8 @@ def print_figure(
23012301
else:
23022302
_bbox_inches_restore = None
23032303

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

lib/matplotlib/figure.py

Lines changed: 94 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,13 @@
3434

3535
from matplotlib.axes import Axes, SubplotBase, subplot_class_factory
3636
from matplotlib.gridspec import GridSpec
37+
from matplotlib.layout_engine import (constrained_layout_engine,
38+
tight_layout_engine, LayoutEngine)
3739
import matplotlib.legend as mlegend
3840
from matplotlib.patches import Rectangle
3941
from matplotlib.text import Text
4042
from matplotlib.transforms import (Affine2D, Bbox, BboxTransformTo,
4143
TransformedBbox)
42-
4344
_log = logging.getLogger(__name__)
4445

4546

@@ -1136,12 +1137,15 @@ def colorbar(self, mappable, cax=None, ax=None, use_gridspec=True, **kw):
11361137
"silence this warning, explicitly pass the 'ax' argument "
11371138
"to colorbar().")
11381139

1140+
if use_gridspec:
1141+
if (self.get_layout_engine() is not None and
1142+
not self.get_layout_engine().colorbar_gridspec):
1143+
use_gridspec = False
11391144
# Store the value of gca so that we can set it back later on.
11401145
if cax is None:
11411146
current_ax = self.gca()
11421147
userax = False
1143-
if (use_gridspec and isinstance(ax, SubplotBase)
1144-
and not self.get_constrained_layout()):
1148+
if (use_gridspec and isinstance(ax, SubplotBase)):
11451149
cax, kw = cbar.make_axes_gridspec(ax, **kw)
11461150
else:
11471151
cax, kw = cbar.make_axes(ax, **kw)
@@ -1189,12 +1193,12 @@ def subplots_adjust(self, left=None, bottom=None, right=None, top=None,
11891193
The height of the padding between subplots,
11901194
as a fraction of the average Axes height.
11911195
"""
1192-
if self.get_constrained_layout():
1193-
self.set_constrained_layout(False)
1196+
if (self.layout_engine is not None and
1197+
not self.layout_engine.adjust_compatible):
11941198
_api.warn_external(
1195-
"This figure was using constrained_layout, but that is "
1199+
"This figure was using a layout engine that is "
11961200
"incompatible with subplots_adjust and/or tight_layout; "
1197-
"disabling constrained_layout.")
1201+
"not calling subplots_adjust.")
11981202
self.subplotpars.update(left, bottom, right, top, wspace, hspace)
11991203
for ax in self.axes:
12001204
if hasattr(ax, 'get_subplotspec'):
@@ -2068,6 +2072,9 @@ def get_constrained_layout_pads(self, relative=False):
20682072
"""
20692073
return self._parent.get_constrained_layout_pads(relative=relative)
20702074

2075+
def get_layout_engine(self):
2076+
return self._parent.get_layout_engine()
2077+
20712078
@property
20722079
def axes(self):
20732080
"""
@@ -2224,24 +2231,31 @@ def __init__(self,
22242231
%(Figure:kwdoc)s
22252232
"""
22262233
super().__init__(**kwargs)
2227-
2234+
self.layout_engine = None
22282235
if layout is not None:
22292236
if tight_layout is not None:
22302237
_api.warn_external(
22312238
"The Figure parameters 'layout' and 'tight_layout' "
22322239
"cannot be used together. Please use 'layout' only.")
2233-
if constrained_layout is not None:
2240+
elif constrained_layout is not None:
22342241
_api.warn_external(
22352242
"The Figure parameters 'layout' and 'constrained_layout' "
22362243
"cannot be used together. Please use 'layout' only.")
2237-
if layout == 'constrained':
2238-
tight_layout = False
2239-
constrained_layout = True
2240-
elif layout == 'tight':
2241-
tight_layout = True
2242-
constrained_layout = False
2243-
else:
2244-
_api.check_in_list(['constrained', 'tight'], layout=layout)
2244+
_api.check_in_list(['constrained', 'tight'], layout=layout)
2245+
self.set_layout_engine(layout=layout)
2246+
elif tight_layout is not None:
2247+
if constrained_layout is not None:
2248+
_api.warn_external(
2249+
"The Figure parameters 'tight_layout' and "
2250+
"'constrained_layout' cannot be used together. Please use "
2251+
"'layout' parameter")
2252+
self.set_layout_engine(layout='tight')
2253+
if isinstance(tight_layout, dict):
2254+
self.get_layout_engine().set_info(tight_layout)
2255+
elif constrained_layout is not None:
2256+
self.set_layout_engine(layout='constrained')
2257+
if isinstance(constrained_layout, dict):
2258+
self.get_layout_engine().set_info(constrained_layout)
22452259

22462260
self.callbacks = cbook.CallbackRegistry()
22472261
# Callbacks traditionally associated with the canvas (and exposed with
@@ -2292,20 +2306,37 @@ def __init__(self,
22922306

22932307
self.subplotpars = subplotpars
22942308

2295-
# constrained_layout:
2296-
self._constrained = False
2297-
2298-
self.set_tight_layout(tight_layout)
2299-
23002309
self._axstack = _AxesStack() # track all figure axes and current axes
23012310
self.clf()
23022311
self._cachedRenderer = None
23032312

2304-
self.set_constrained_layout(constrained_layout)
2305-
23062313
# list of child gridspecs for this figure
23072314
self._gridspecs = []
23082315

2316+
def set_layout_engine(self, layout=None, **kwargs):
2317+
"""
2318+
Set the layout engine... FIXME
2319+
"""
2320+
if layout is None:
2321+
if mpl.rcParams['figure.autolayout']:
2322+
layout = 'tight'
2323+
elif mpl.rcParams['figure.constrained_layout.use']:
2324+
layout = 'constrained'
2325+
else:
2326+
self.layout_engine = None
2327+
return
2328+
if layout == 'tight':
2329+
self.layout_engine = tight_layout_engine(self, **kwargs)
2330+
elif layout == 'constrained':
2331+
self.layout_engine = constrained_layout_engine(self, **kwargs)
2332+
elif isinstance(layout, LayoutEngine):
2333+
self.layout_engine = layout
2334+
else:
2335+
raise ValueError(f"Invalid value for 'layout': {layout!r}")
2336+
2337+
def get_layout_engine(self):
2338+
return self.layout_engine
2339+
23092340
# TODO: I'd like to dynamically add the _repr_html_ method
23102341
# to the figure in the right context, but then IPython doesn't
23112342
# use it, for some reason.
@@ -2395,8 +2426,9 @@ def _set_dpi(self, dpi, forward=True):
23952426

23962427
def get_tight_layout(self):
23972428
"""Return whether `.tight_layout` is called when drawing."""
2398-
return self._tight
2429+
return isinstance(self.layout_engine, tight_layout_engine)
23992430

2431+
@_api.deprecated("3.6", alternative="set_layout_engine")
24002432
def set_tight_layout(self, tight):
24012433
"""
24022434
Set whether and how `.tight_layout` is called when drawing.
@@ -2411,8 +2443,9 @@ def set_tight_layout(self, tight):
24112443
"""
24122444
if tight is None:
24132445
tight = mpl.rcParams['figure.autolayout']
2414-
self._tight = bool(tight)
2415-
self._tight_parameters = tight if isinstance(tight, dict) else {}
2446+
_tight_parameters = tight if isinstance(tight, dict) else {}
2447+
if bool(tight):
2448+
self.layout_engine = tight_layout_engine(self, _tight_parameters)
24162449
self.stale = True
24172450

24182451
def get_constrained_layout(self):
@@ -2421,8 +2454,9 @@ def get_constrained_layout(self):
24212454
24222455
See :doc:`/tutorials/intermediate/constrainedlayout_guide`.
24232456
"""
2424-
return self._constrained
2457+
return isinstance(self.layout_engine, constrained_layout_engine)
24252458

2459+
@_api.deprecated("3.6", alternative="set_layout_engine('constrained')")
24262460
def set_constrained_layout(self, constrained):
24272461
"""
24282462
Set whether ``constrained_layout`` is used upon drawing. If None,
@@ -2439,22 +2473,17 @@ def set_constrained_layout(self, constrained):
24392473
----------
24402474
constrained : bool or dict or None
24412475
"""
2442-
self._constrained_layout_pads = dict()
2443-
self._constrained_layout_pads['w_pad'] = None
2444-
self._constrained_layout_pads['h_pad'] = None
2445-
self._constrained_layout_pads['wspace'] = None
2446-
self._constrained_layout_pads['hspace'] = None
24472476
if constrained is None:
24482477
constrained = mpl.rcParams['figure.constrained_layout.use']
2449-
self._constrained = bool(constrained)
2450-
if isinstance(constrained, dict):
2451-
self.set_constrained_layout_pads(**constrained)
2452-
else:
2453-
self.set_constrained_layout_pads()
2478+
_constrained = bool(constrained)
2479+
_parameters = constrained if isinstance(constrained, dict) else {}
2480+
if _constrained:
2481+
self.layout_engine = constrained_layout_engine(self, _parameters)
24542482
self.stale = True
24552483

2456-
def set_constrained_layout_pads(self, *, w_pad=None, h_pad=None,
2457-
wspace=None, hspace=None):
2484+
@_api.deprecated(
2485+
"3.6", alternative="figure.layout_engine.set_info()")
2486+
def set_constrained_layout_pads(self, **kwargs):
24582487
"""
24592488
Set padding for ``constrained_layout``.
24602489
@@ -2482,18 +2511,13 @@ def set_constrained_layout_pads(self, *, w_pad=None, h_pad=None,
24822511
subplot width. The total padding ends up being h_pad + hspace.
24832512
24842513
"""
2514+
if isinstance(self.layout_engine, constrained_layout_engine):
2515+
self.layout_engine.set_info(**kwargs)
24852516

2486-
for name, size in zip(['w_pad', 'h_pad', 'wspace', 'hspace'],
2487-
[w_pad, h_pad, wspace, hspace]):
2488-
if size is not None:
2489-
self._constrained_layout_pads[name] = size
2490-
else:
2491-
self._constrained_layout_pads[name] = (
2492-
mpl.rcParams[f'figure.constrained_layout.{name}'])
2493-
2517+
@_api.deprecated("3.6", alternative="fig.layout_engine.get_info()")
24942518
def get_constrained_layout_pads(self, relative=False):
24952519
"""
2496-
Get padding for ``constrained_layout``.
2520+
Get padding for ``constrained_layout`` if it is the ``layout_engine``.
24972521
24982522
Returns a list of ``w_pad, h_pad`` in inches and
24992523
``wspace`` and ``hspace`` as fractions of the subplot.
@@ -2505,13 +2529,16 @@ def get_constrained_layout_pads(self, relative=False):
25052529
relative : bool
25062530
If `True`, then convert from inches to figure relative.
25072531
"""
2508-
w_pad = self._constrained_layout_pads['w_pad']
2509-
h_pad = self._constrained_layout_pads['h_pad']
2510-
wspace = self._constrained_layout_pads['wspace']
2511-
hspace = self._constrained_layout_pads['hspace']
2532+
if not isinstance(self.layout_engine, constrained_layout_engine):
2533+
return None, None, None, None
2534+
info = self.layout_engine.get_info()
2535+
w_pad = info['w_pad']
2536+
h_pad = info['h_pad']
2537+
wspace = info['wspace']
2538+
hspace = info['hspace']
25122539

25132540
if relative and (w_pad is not None or h_pad is not None):
2514-
renderer = _get_renderer(self)
2541+
renderer = _get_renderer(self).dpi
25152542
dpi = renderer.dpi
25162543
w_pad = w_pad * dpi / renderer.width
25172544
h_pad = h_pad * dpi / renderer.height
@@ -2783,14 +2810,11 @@ def draw(self, renderer):
27832810
return
27842811

27852812
artists = self._get_draw_artists(renderer)
2786-
27872813
try:
27882814
renderer.open_group('figure', gid=self.get_gid())
2789-
if self.get_constrained_layout() and self.axes:
2790-
self.execute_constrained_layout(renderer)
2791-
if self.get_tight_layout() and self.axes:
2815+
if self.axes and self.layout_engine is not None:
27922816
try:
2793-
self.tight_layout(**self._tight_parameters)
2817+
self.layout_engine.execute()
27942818
except ValueError:
27952819
pass
27962820
# ValueError can occur when resizing a window.
@@ -3123,6 +3147,7 @@ def handler(ev):
31233147

31243148
return None if event is None else event.name == "key_press_event"
31253149

3150+
@_api.deprecated("3.6", alternative="figure.layout_engine.execute()")
31263151
def execute_constrained_layout(self, renderer=None):
31273152
"""
31283153
Use ``layoutgrid`` to determine pos positions within Axes.
@@ -3133,22 +3158,11 @@ def execute_constrained_layout(self, renderer=None):
31333158
-------
31343159
layoutgrid : private debugging object
31353160
"""
3161+
if not isinstance(self.layout_engine, constrained_layout_engine):
3162+
return None
3163+
return self.layout_engine.execute()
31363164

3137-
from matplotlib._constrained_layout import do_constrained_layout
3138-
3139-
_log.debug('Executing constrainedlayout')
3140-
w_pad, h_pad, wspace, hspace = self.get_constrained_layout_pads()
3141-
# convert to unit-relative lengths
3142-
fig = self
3143-
width, height = fig.get_size_inches()
3144-
w_pad = w_pad / width
3145-
h_pad = h_pad / height
3146-
if renderer is None:
3147-
renderer = _get_renderer(fig)
3148-
return do_constrained_layout(fig, renderer, h_pad, w_pad,
3149-
hspace, wspace)
3150-
3151-
def tight_layout(self, *, pad=1.08, h_pad=None, w_pad=None, rect=None):
3165+
def tight_layout(self, **kwargs):
31523166
"""
31533167
Adjust the padding between and around subplots.
31543168
@@ -3170,24 +3184,19 @@ def tight_layout(self, *, pad=1.08, h_pad=None, w_pad=None, rect=None):
31703184
31713185
See Also
31723186
--------
3173-
.Figure.set_tight_layout
3187+
.Figure.set_layout_engine
31743188
.pyplot.tight_layout
31753189
"""
3176-
from contextlib import nullcontext
3177-
from .tight_layout import (
3178-
get_subplotspec_list, get_tight_layout_figure)
3190+
from .tight_layout import get_subplotspec_list
31793191
subplotspec_list = get_subplotspec_list(self.axes)
31803192
if None in subplotspec_list:
31813193
_api.warn_external("This figure includes Axes that are not "
31823194
"compatible with tight_layout, so results "
31833195
"might be incorrect.")
3184-
renderer = _get_renderer(self)
3185-
with getattr(renderer, "_draw_disabled", nullcontext)():
3186-
kwargs = get_tight_layout_figure(
3187-
self, self.axes, subplotspec_list, renderer,
3188-
pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect)
3189-
if kwargs:
3190-
self.subplots_adjust(**kwargs)
3196+
# note that here we do not _set_ the figures engine to tight_layout
3197+
# but rather just perform the layout in place for back compatibility.
3198+
engine = tight_layout_engine(fig=self, **kwargs)
3199+
engine.execute()
31913200

31923201

31933202
def figaspect(arg):

0 commit comments

Comments
 (0)