Skip to content

Merge SubplotBase into AxesBase. #23573

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 1 commit into from
Oct 20, 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
16 changes: 4 additions & 12 deletions doc/api/axes_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,6 @@ The Axes class
:no-undoc-members:
:show-inheritance:


Subplots
========

.. autosummary::
:toctree: _as_gen
:template: autosummary.rst
:nosignatures:

SubplotBase
subplot_class_factory

Plotting
========

Expand Down Expand Up @@ -313,6 +301,7 @@ Axis labels, title, and legend
Axes.get_xlabel
Axes.set_ylabel
Axes.get_ylabel
Axes.label_outer

Axes.set_title
Axes.get_title
Expand Down Expand Up @@ -484,6 +473,9 @@ Axes position
Axes.get_axes_locator
Axes.set_axes_locator

Axes.get_subplotspec
Axes.set_subplotspec

Axes.reset_position

Axes.get_position
Expand Down
7 changes: 7 additions & 0 deletions doc/api/next_api_changes/behavior/23573-AL.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
All Axes have ``get_subplotspec`` and ``get_gridspec`` methods now, which returns None for Axes not positioned via a gridspec
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Previously, this method was only present for Axes positioned via a gridspec.
Following this change, checking ``hasattr(ax, "get_gridspec")`` should now be
replaced by ``ax.get_gridspec() is not None``. For compatibility with older
Matplotlib releases, one can also check
``hasattr(ax, "get_gridspec") and ax.get_gridspec() is not None``.
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ are deprecated. Panning and zooming are now implemented using the

Passing None to various Axes subclass factories
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for passing ``None`` as base class to `.axes.subplot_class_factory`,
Support for passing ``None`` as base class to ``axes.subplot_class_factory``,
``axes_grid1.parasite_axes.host_axes_class_factory``,
``axes_grid1.parasite_axes.host_subplot_class_factory``,
``axes_grid1.parasite_axes.parasite_axes_class_factory``, and
Expand Down
4 changes: 2 additions & 2 deletions doc/api/prev_api_changes/api_changes_3.4.0/deprecations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ Subplot-related attributes and methods
Some ``SubplotBase`` methods and attributes have been deprecated and/or moved
to `.SubplotSpec`:

- ``get_geometry`` (use `.SubplotBase.get_subplotspec` instead),
- ``change_geometry`` (use `.SubplotBase.set_subplotspec` instead),
- ``get_geometry`` (use ``SubplotBase.get_subplotspec`` instead),
- ``change_geometry`` (use ``SubplotBase.set_subplotspec`` instead),
Comment on lines +41 to +42
Copy link
Member

@QuLogic QuLogic Oct 21, 2022

Choose a reason for hiding this comment

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

The removal message in 3.6 API changes was not updated like here, which is breaking docs. But should these alternatives really be made code-style instead of linking to the Axes methods instead?

Copy link
Member

Choose a reason for hiding this comment

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

Fair question. I consider the changelog a historic document that reflects the state at that time. We also don't go back and change other things in changelog, that are not correct anymore. So, code-style is correct.

Copy link
Member

Choose a reason for hiding this comment

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

OK, #24236

- ``is_first_row``, ``is_last_row``, ``is_first_col``, ``is_last_col`` (use the
corresponding methods on the `.SubplotSpec` instance instead),
- ``update_params`` (now a no-op),
Expand Down
6 changes: 3 additions & 3 deletions doc/users/prev_whats_new/whats_new_3.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,10 @@ independent on the axes size or units. To revert to the previous behaviour
set the axes' aspect ratio to automatic by using ``ax.set_aspect("auto")`` or
``plt.axis("auto")``.

Add ``ax.get_gridspec`` to `.SubplotBase`
-----------------------------------------
Add ``ax.get_gridspec`` to ``SubplotBase``
------------------------------------------

New method `.SubplotBase.get_gridspec` is added so that users can
New method ``SubplotBase.get_gridspec`` is added so that users can
easily get the gridspec that went into making an axes:

.. code::
Expand Down
39 changes: 18 additions & 21 deletions lib/matplotlib/_constrained_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,8 @@ def make_layoutgrids(fig, layoutgrids, rect=(0, 0, 1, 1)):

# for each axes at the local level add its gridspec:
for ax in fig._localaxes:
if hasattr(ax, 'get_subplotspec'):
gs = ax.get_subplotspec().get_gridspec()
gs = ax.get_gridspec()
if gs is not None:
layoutgrids = make_layoutgrids_gs(layoutgrids, gs)

return layoutgrids
Expand Down Expand Up @@ -248,24 +248,22 @@ def check_no_collapsed_axes(layoutgrids, fig):
ok = check_no_collapsed_axes(layoutgrids, sfig)
if not ok:
return False

for ax in fig.axes:
if hasattr(ax, 'get_subplotspec'):
gs = ax.get_subplotspec().get_gridspec()
if gs in layoutgrids:
lg = layoutgrids[gs]
for i in range(gs.nrows):
for j in range(gs.ncols):
bb = lg.get_inner_bbox(i, j)
if bb.width <= 0 or bb.height <= 0:
return False
gs = ax.get_gridspec()
if gs in layoutgrids: # also implies gs is not None.
lg = layoutgrids[gs]
for i in range(gs.nrows):
for j in range(gs.ncols):
bb = lg.get_inner_bbox(i, j)
if bb.width <= 0 or bb.height <= 0:
return False
return True


def compress_fixed_aspect(layoutgrids, fig):
gs = None
for ax in fig.axes:
if not hasattr(ax, 'get_subplotspec'):
if ax.get_subplotspec() is None:
continue
ax.apply_aspect()
sub = ax.get_subplotspec()
Expand Down Expand Up @@ -357,7 +355,7 @@ def make_layout_margins(layoutgrids, fig, renderer, *, w_pad=0, h_pad=0,
layoutgrids[sfig].parent.edit_outer_margin_mins(margins, ss)

for ax in fig._localaxes:
if not hasattr(ax, 'get_subplotspec') or not ax.get_in_layout():
if not ax.get_subplotspec() or not ax.get_in_layout():
continue

ss = ax.get_subplotspec()
Expand Down Expand Up @@ -488,8 +486,8 @@ def match_submerged_margins(layoutgrids, fig):
for sfig in fig.subfigs:
match_submerged_margins(layoutgrids, sfig)

axs = [a for a in fig.get_axes() if (hasattr(a, 'get_subplotspec')
and a.get_in_layout())]
axs = [a for a in fig.get_axes()
if a.get_subplotspec() is not None and a.get_in_layout()]

for ax1 in axs:
ss1 = ax1.get_subplotspec()
Expand Down Expand Up @@ -620,7 +618,7 @@ def reposition_axes(layoutgrids, fig, renderer, *,
wspace=wspace, hspace=hspace)

for ax in fig._localaxes:
if not hasattr(ax, 'get_subplotspec') or not ax.get_in_layout():
if ax.get_subplotspec() is None or not ax.get_in_layout():
continue

# grid bbox is in Figure coordinates, but we specify in panel
Expand Down Expand Up @@ -742,10 +740,9 @@ def reset_margins(layoutgrids, fig):
for sfig in fig.subfigs:
reset_margins(layoutgrids, sfig)
for ax in fig.axes:
if hasattr(ax, 'get_subplotspec') and ax.get_in_layout():
ss = ax.get_subplotspec()
gs = ss.get_gridspec()
if gs in layoutgrids:
if ax.get_in_layout():
gs = ax.get_gridspec()
if gs in layoutgrids: # also implies gs is not None.
layoutgrids[gs].reset_margins()
layoutgrids[fig].reset_margins()

Expand Down
18 changes: 17 additions & 1 deletion lib/matplotlib/axes/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,18 @@
from ._subplots import *
from . import _base
from ._axes import *

# Backcompat.
Copy link
Member

Choose a reason for hiding this comment

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

IMHO we should get rid of this stuff eventually.

As a first step, they should become pending deprecated and we should communicate that people should move away from subplot stuff in the change notes.

Do we want to deprecate this stuff?

from ._axes import Axes as Subplot


class _SubplotBaseMeta(type):
def __instancecheck__(self, obj):
return (isinstance(obj, _base._AxesBase)
and obj.get_subplotspec() is not None)


class SubplotBase(metaclass=_SubplotBaseMeta):
pass


def subplot_class_factory(cls): return cls
137 changes: 119 additions & 18 deletions lib/matplotlib/axes/_base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from collections.abc import MutableSequence
from collections.abc import Iterable, MutableSequence
from contextlib import ExitStack
import functools
import inspect
Expand All @@ -18,6 +18,7 @@
import matplotlib.collections as mcoll
import matplotlib.colors as mcolors
import matplotlib.font_manager as font_manager
from matplotlib.gridspec import SubplotSpec
import matplotlib.image as mimage
import matplotlib.lines as mlines
import matplotlib.patches as mpatches
Expand Down Expand Up @@ -569,8 +570,8 @@ def __str__(self):
return "{0}({1[0]:g},{1[1]:g};{1[2]:g}x{1[3]:g})".format(
type(self).__name__, self._position.bounds)

def __init__(self, fig, rect,
*,
def __init__(self, fig,
*args,
facecolor=None, # defaults to rc axes.facecolor
frameon=True,
sharex=None, # use Axes instance's xaxis info
Expand All @@ -589,9 +590,18 @@ def __init__(self, fig, rect,
fig : `~matplotlib.figure.Figure`
The Axes is built in the `.Figure` *fig*.

rect : tuple (left, bottom, width, height).
The Axes is built in the rectangle *rect*. *rect* is in
`.Figure` coordinates.
*args
Copy link
Member

Choose a reason for hiding this comment

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

Is this needed to unite the Axes and Subplot constuctor APIs? That's quite ugly. We should consider to migrate three numbers to "one tuple of three numbers" so that we have always exactly one argument rect back and can save the additional rect in kwargs handling.

But that may be for later.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, this is super ugly and I do want to move to "one tuple of 3 numbers", but that cannot be done with yet another deprecation cycle which can be done later.

``*args`` can be a single ``(left, bottom, width, height)``
rectangle or a single `.Bbox`. This specifies the rectangle (in
figure coordinates) where the Axes is positioned.

``*args`` can also consist of three numbers or a single three-digit
Copy link
Member

Choose a reason for hiding this comment

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

We should move away from three separate number parameters towards one tuple, so that we can have a single parameter again. I think this should not be too annoying downstream because few people instantiate Axes directly.

This would need a pending deprecation. On the downside, we'd have for the one parameter a 4-tuple meaning a rect and a 3-tuple meaning a subplot spec, but that seems bearable.

number; in the latter case, the digits are considered as
independent numbers. The numbers are interpreted as ``(nrows,
ncols, index)``: ``(nrows, ncols)`` specifies the size of an array
of subplots, and ``index`` is the 1-based index of the subplot
being created. Finally, ``*args`` can also directly be a
`.SubplotSpec` instance.

sharex, sharey : `~.axes.Axes`, optional
The x or y `~.matplotlib.axis` is shared with the x or
Expand All @@ -616,10 +626,21 @@ def __init__(self, fig, rect,
"""

super().__init__()
if isinstance(rect, mtransforms.Bbox):
self._position = rect
if "rect" in kwargs:
if args:
raise TypeError(
"'rect' cannot be used together with positional arguments")
rect = kwargs.pop("rect")
_api.check_isinstance((mtransforms.Bbox, Iterable), rect=rect)
args = (rect,)
subplotspec = None
if len(args) == 1 and isinstance(args[0], mtransforms.Bbox):
self._position = args[0]
elif len(args) == 1 and np.iterable(args[0]):
self._position = mtransforms.Bbox.from_bounds(*args[0])
else:
self._position = mtransforms.Bbox.from_bounds(*rect)
self._position = self._originalPosition = mtransforms.Bbox.unit()
subplotspec = SubplotSpec._from_subplot_args(fig, args)
if self._position.width < 0 or self._position.height < 0:
raise ValueError('Width and height specified must be non-negative')
self._originalPosition = self._position.frozen()
Expand All @@ -632,8 +653,16 @@ def __init__(self, fig, rect,
self._sharey = sharey
self.set_label(label)
self.set_figure(fig)
# The subplotspec needs to be set after the figure (so that
# figure-level subplotpars are taken into account), but the figure
# needs to be set after self._position is initialized.
if subplotspec:
self.set_subplotspec(subplotspec)
else:
self._subplotspec = None
self.set_box_aspect(box_aspect)
self._axes_locator = None # Optionally set via update(kwargs).

# placeholder for any colorbars added that use this Axes.
# (see colorbar.py):
self._colorbars = []
Expand Down Expand Up @@ -737,6 +766,19 @@ def __repr__(self):
fields += [f"{name}label={axis.get_label().get_text()!r}"]
return f"<{self.__class__.__name__}: " + ", ".join(fields) + ">"

def get_subplotspec(self):
"""Return the `.SubplotSpec` associated with the subplot, or None."""
return self._subplotspec

def set_subplotspec(self, subplotspec):
"""Set the `.SubplotSpec`. associated with the subplot."""
self._subplotspec = subplotspec
self._set_position(subplotspec.get_position(self.figure))

def get_gridspec(self):
"""Return the `.GridSpec` associated with the subplot, or None."""
return self._subplotspec.get_gridspec() if self._subplotspec else None

@_api.delete_parameter("3.6", "args")
@_api.delete_parameter("3.6", "kwargs")
def get_window_extent(self, renderer=None, *args, **kwargs):
Expand Down Expand Up @@ -4424,17 +4466,23 @@ def get_tightbbox(self, renderer=None, call_axes_locator=True,

def _make_twin_axes(self, *args, **kwargs):
"""Make a twinx Axes of self. This is used for twinx and twiny."""
# Typically, SubplotBase._make_twin_axes is called instead of this.
if 'sharex' in kwargs and 'sharey' in kwargs:
raise ValueError("Twinned Axes may share only one axis")
ax2 = self.figure.add_axes(
self.get_position(True), *args, **kwargs,
axes_locator=_TransformedBoundsLocator(
[0, 0, 1, 1], self.transAxes))
# The following line is added in v2.2 to avoid breaking Seaborn,
# which currently uses this internal API.
if kwargs["sharex"] is not self and kwargs["sharey"] is not self:
raise ValueError("Twinned Axes may share only one axis")
ss = self.get_subplotspec()
if ss:
twin = self.figure.add_subplot(ss, *args, **kwargs)
else:
twin = self.figure.add_axes(
self.get_position(True), *args, **kwargs,
axes_locator=_TransformedBoundsLocator(
[0, 0, 1, 1], self.transAxes))
self.set_adjustable('datalim')
ax2.set_adjustable('datalim')
self._twinned_axes.join(self, ax2)
return ax2
twin.set_adjustable('datalim')
self._twinned_axes.join(self, twin)
return twin

def twinx(self):
"""
Expand Down Expand Up @@ -4502,3 +4550,56 @@ def get_shared_x_axes(self):
def get_shared_y_axes(self):
"""Return an immutable view on the shared y-axes Grouper."""
return cbook.GrouperView(self._shared_axes["y"])

def label_outer(self):
"""
Only show "outer" labels and tick labels.

x-labels are only kept for subplots on the last row (or first row, if
labels are on the top side); y-labels only for subplots on the first
column (or last column, if labels are on the right side).
"""
self._label_outer_xaxis(check_patch=False)
self._label_outer_yaxis(check_patch=False)

def _label_outer_xaxis(self, *, check_patch):
# see documentation in label_outer.
if check_patch and not isinstance(self.patch, mpl.patches.Rectangle):
return
ss = self.get_subplotspec()
if not ss:
return
label_position = self.xaxis.get_label_position()
if not ss.is_first_row(): # Remove top label/ticklabels/offsettext.
if label_position == "top":
self.set_xlabel("")
self.xaxis.set_tick_params(which="both", labeltop=False)
if self.xaxis.offsetText.get_position()[1] == 1:
self.xaxis.offsetText.set_visible(False)
if not ss.is_last_row(): # Remove bottom label/ticklabels/offsettext.
if label_position == "bottom":
self.set_xlabel("")
self.xaxis.set_tick_params(which="both", labelbottom=False)
if self.xaxis.offsetText.get_position()[1] == 0:
self.xaxis.offsetText.set_visible(False)

def _label_outer_yaxis(self, *, check_patch):
# see documentation in label_outer.
if check_patch and not isinstance(self.patch, mpl.patches.Rectangle):
return
ss = self.get_subplotspec()
if not ss:
return
label_position = self.yaxis.get_label_position()
if not ss.is_first_col(): # Remove left label/ticklabels/offsettext.
if label_position == "left":
self.set_ylabel("")
self.yaxis.set_tick_params(which="both", labelleft=False)
if self.yaxis.offsetText.get_position()[0] == 0:
self.yaxis.offsetText.set_visible(False)
if not ss.is_last_col(): # Remove right label/ticklabels/offsettext.
if label_position == "right":
self.set_ylabel("")
self.yaxis.set_tick_params(which="both", labelright=False)
if self.yaxis.offsetText.get_position()[0] == 1:
self.yaxis.offsetText.set_visible(False)
Loading