Skip to content

Layout engine #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 45 additions & 30 deletions lib/matplotlib/layout_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,29 +30,39 @@ class LayoutEngine:
Base class for Matplotlib layout engines.

A layout engine can be passed to a figure at instantiation or at any time
with `~.figure.Figure.set_layout_engine`. However, note note that layout
engines affect the creation of colorbars, so
`~.figure.Figure.set_layout_engine` should be called before any colorbars
are created.

Once attached to a figure, the layout engine ``execute`` function is called
at draw time by `~.figure.Figure.draw`, providing a special draw-time hook.

Currently, there are two properties of ``LayoutEngine`` classes that are
consulted while manipulating the figure. ``engine.colorbar_gridspec``
tells `.Figure.colorbar` whether to make the axes using the gridspec
method (see `.colorbar.make_axes_gridspec`) or not
(see `.colorbar.make_axes`); `.ConstrainedLayoutEngine` sets this to
*False*, `.TightLayoutEngine` to *True*. The second property is
``engine.adjust_compatible`` that stops `.Figure.subplots_adjust` from
being run if it is not compatible with the layout engine
(`.ConstrainedLayoutEngine` sets this to *False* also).
with `~.figure.Figure.set_layout_engine`. Once attached to a figure, the
layout engine ``execute`` function is called at draw time by
`~.figure.Figure.draw`, providing a special draw-time hook.

.. note ::

However, note that layout engines affect the creation of colorbars, so
`~.figure.Figure.set_layout_engine` should be called before any
colorbars are created.

Currently, there are two properties of `LayoutEngine` classes that are
consulted while manipulating the figure:

- ``engine.colorbar_gridspec`` tells `.Figure.colorbar` whether to make the
axes using the gridspec method (see `.colorbar.make_axes_gridspec`) or
not (see `.colorbar.make_axes`);
- ``engine.adjust_compatible`` stops `.Figure.subplots_adjust` from being run
if it is not compatible with the layout engine.

To implement a custom `LayoutEngine`:

1. override ``_adjust_compatible`` and ``_colorbar_gridspec``
2. override `LayoutEngine.set` to update *self._params*
3. override `LayoutEngine.execute` with your implementation

"""
# override these is sub-class
_adjust_compatible = None
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for my education, why are these better defined here than in the init?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a couple of marginal things for me.

Defining them in __init__ sub-classes will either have to be sure to call __super__() (which they do have to here for other reasons so this is a bit of a wash here) or remember to define these values directly. By putting them on the class object you have taken a bit of a burden / defused a trap for the down stream user. In the case of a user-implemented subclass that does not want to make itself configurable at init time (because it has no configuration or just because), then the do not even have to define an __init__ with these values as class level attributes.

Another advantage of this is because these really are class-constants (at least for these two) in the sense that these should never change at run time. Putting them here indicates that and makes it easy to, when skimming the code, see these right away as different "class state things" not "instance state things". It is a small thing, but it gives a hint to someone reading this to implement their own.

By making these default to None (and checking it in the properties!) you force a down-stream implementer to think about this once to make a choice.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, thanks for the insight!

_colorbar_gridspec = None

def __init__(self):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._params = {}
self._adjust_compatible = True
self._colorbar_gridspec = True

def set(self, **kwargs):
raise NotImplementedError
Expand All @@ -63,6 +73,8 @@ def colorbar_gridspec(self):
Return a boolean if the layout engine creates colorbars using a
gridspec.
"""
if self._colorbar_gridspec is None:
raise NotImplementedError
return self._colorbar_gridspec

@property
Expand All @@ -71,13 +83,15 @@ def adjust_compatible(self):
Return a boolean if the layout engine is compatible with
`~.Figure.subplots_adjust`.
"""
if self._adjust_compatible is None:
raise NotImplementedError
return self._adjust_compatible

def get(self):
"""
Return the parameters for the layout engine.
Return copy of the parameters for the layout engine.
"""
return self._params
return dict(self._params)

def execute(self, fig):
"""
Expand All @@ -92,9 +106,11 @@ class TightLayoutEngine(LayoutEngine):
Implements the ``tight_layout`` geometry management. See
:doc:`/tutorials/intermediate/tight_layout_guide` for details.
"""
_adjust_compatible = True
_colorbar_gridspec = True

def __init__(self, *, pad=1.08, h_pad=None, w_pad=None,
rect=(0, 0, 1, 1)):
rect=(0, 0, 1, 1), **kwargs):
"""
Initialize tight_layout engine.

Expand All @@ -111,13 +127,11 @@ def __init__(self, *, pad=1.08, h_pad=None, w_pad=None,
coordinates that the subplots (including labels)
will fit into. Defaults to using the entire figure.
"""
super().__init__()
super().__init__(**kwargs)
for td in ['pad', 'h_pad', 'w_pad', 'rect']:
# initialize these in case None is passed in above:
self._params[td] = None
self.set(pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect)
self._adjust_compatible = True
self._colorbar_gridspec = True

def execute(self, fig):
"""
Expand Down Expand Up @@ -160,8 +174,11 @@ class ConstrainedLayoutEngine(LayoutEngine):
:doc:`/tutorials/intermediate/constrained_layout_guide` for details.
"""

_adjust_compatible = False
_colorbar_gridspec = False

def __init__(self, *, h_pad=None, w_pad=None,
hspace=None, wspace=None):
hspace=None, wspace=None, **kwargs):
"""
Initialize ``constrained_layout`` settings.

Expand All @@ -180,16 +197,14 @@ def __init__(self, *, h_pad=None, w_pad=None,
Default to :rc:`figure.constrained_layout.hspace` and
:rc:`figure.constrained_layout.wspace`.
"""
super().__init__()
super().__init__(**kwargs)
# set the defaults:
self.set(w_pad=mpl.rcParams['figure.constrained_layout.w_pad'],
h_pad=mpl.rcParams['figure.constrained_layout.h_pad'],
wspace=mpl.rcParams['figure.constrained_layout.wspace'],
hspace=mpl.rcParams['figure.constrained_layout.hspace'])
# set anything that was passed in (None will be ignored):
self.set(w_pad=w_pad, h_pad=h_pad, wspace=wspace, hspace=hspace)
self._adjust_compatible = False
self._colorbar_gridspec = False

def execute(self, fig):
"""
Expand Down
3 changes: 2 additions & 1 deletion lib/matplotlib/tests/test_figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -585,10 +585,11 @@ def test_invalid_layouts():

# Using layout + (tight|constrained)_layout warns, but the former takes
# precedence.
wst = "The Figure parameters 'layout' and 'tight_layout' or "
wst = "The Figure parameters 'layout' and 'tight_layout'"
with pytest.warns(UserWarning, match=wst):
fig = Figure(layout='tight', tight_layout=False)
assert isinstance(fig.get_layout_engine(), TightLayoutEngine)
wst = "The Figure parameters 'layout' and 'constrained_layout'"
with pytest.warns(UserWarning, match=wst):
fig = Figure(layout='constrained', constrained_layout=False)
assert not isinstance(fig.get_layout_engine(), TightLayoutEngine)
Expand Down