Skip to content

Commit a0a43b2

Browse files
committed
Merge SubplotBase into AxesBase.
1 parent 69cf385 commit a0a43b2

27 files changed

+245
-297
lines changed

doc/api/axes_api.rst

+4-12
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,6 @@ The Axes class
2727
:no-undoc-members:
2828
:show-inheritance:
2929

30-
31-
Subplots
32-
========
33-
34-
.. autosummary::
35-
:toctree: _as_gen
36-
:template: autosummary.rst
37-
:nosignatures:
38-
39-
SubplotBase
40-
subplot_class_factory
41-
4230
Plotting
4331
========
4432

@@ -313,6 +301,7 @@ Axis labels, title, and legend
313301
Axes.get_xlabel
314302
Axes.set_ylabel
315303
Axes.get_ylabel
304+
Axes.label_outer
316305

317306
Axes.set_title
318307
Axes.get_title
@@ -484,6 +473,9 @@ Axes position
484473
Axes.get_axes_locator
485474
Axes.set_axes_locator
486475

476+
Axes.get_subplotspec
477+
Axes.set_subplotspec
478+
487479
Axes.reset_position
488480

489481
Axes.get_position
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
All Axes have a ``get_subplotspec`` method now, which returns None for Axes not positioned via a gridspec
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
Previously, this method was only present for Axes positioned via a gridspec.
4+
Following this change, checking ``hasattr(ax, "get_gridspec")`` should now be
5+
replaced by ``ax.get_gridspec() is not None``.

doc/api/prev_api_changes/api_changes_3.3.0/deprecations.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ are deprecated. Panning and zooming are now implemented using the
328328

329329
Passing None to various Axes subclass factories
330330
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
331-
Support for passing ``None`` as base class to `.axes.subplot_class_factory`,
331+
Support for passing ``None`` as base class to ``axes.subplot_class_factory``,
332332
``axes_grid1.parasite_axes.host_axes_class_factory``,
333333
``axes_grid1.parasite_axes.host_subplot_class_factory``,
334334
``axes_grid1.parasite_axes.parasite_axes_class_factory``, and

doc/api/prev_api_changes/api_changes_3.4.0/deprecations.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ Subplot-related attributes and methods
3838
Some ``SubplotBase`` methods and attributes have been deprecated and/or moved
3939
to `.SubplotSpec`:
4040

41-
- ``get_geometry`` (use `.SubplotBase.get_subplotspec` instead),
42-
- ``change_geometry`` (use `.SubplotBase.set_subplotspec` instead),
41+
- ``get_geometry`` (use ``SubplotBase.get_subplotspec`` instead),
42+
- ``change_geometry`` (use ``SubplotBase.set_subplotspec`` instead),
4343
- ``is_first_row``, ``is_last_row``, ``is_first_col``, ``is_last_col`` (use the
4444
corresponding methods on the `.SubplotSpec` instance instead),
4545
- ``update_params`` (now a no-op),

doc/users/prev_whats_new/whats_new_3.0.rst

+3-3
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,10 @@ independent on the axes size or units. To revert to the previous behaviour
141141
set the axes' aspect ratio to automatic by using ``ax.set_aspect("auto")`` or
142142
``plt.axis("auto")``.
143143

144-
Add ``ax.get_gridspec`` to `.SubplotBase`
145-
-----------------------------------------
144+
Add ``ax.get_gridspec`` to ``SubplotBase``
145+
------------------------------------------
146146

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

150150
.. code::

lib/matplotlib/_constrained_layout.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ def make_layoutgrids(fig, layoutgrids, rect=(0, 0, 1, 1)):
187187

188188
# for each axes at the local level add its gridspec:
189189
for ax in fig._localaxes:
190-
if hasattr(ax, 'get_subplotspec'):
190+
if ax.get_subplotspec():
191191
gs = ax.get_subplotspec().get_gridspec()
192192
layoutgrids = make_layoutgrids_gs(layoutgrids, gs)
193193

@@ -250,7 +250,7 @@ def check_no_collapsed_axes(layoutgrids, fig):
250250
return False
251251

252252
for ax in fig.axes:
253-
if hasattr(ax, 'get_subplotspec'):
253+
if ax.get_subplotspec():
254254
gs = ax.get_subplotspec().get_gridspec()
255255
if gs in layoutgrids:
256256
lg = layoutgrids[gs]
@@ -265,7 +265,7 @@ def check_no_collapsed_axes(layoutgrids, fig):
265265
def compress_fixed_aspect(layoutgrids, fig):
266266
gs = None
267267
for ax in fig.axes:
268-
if not hasattr(ax, 'get_subplotspec'):
268+
if not ax.get_subplotspec():
269269
continue
270270
ax.apply_aspect()
271271
sub = ax.get_subplotspec()
@@ -357,7 +357,7 @@ def make_layout_margins(layoutgrids, fig, renderer, *, w_pad=0, h_pad=0,
357357
layoutgrids[sfig].parent.edit_outer_margin_mins(margins, ss)
358358

359359
for ax in fig._localaxes:
360-
if not hasattr(ax, 'get_subplotspec') or not ax.get_in_layout():
360+
if not ax.get_subplotspec() or not ax.get_in_layout():
361361
continue
362362

363363
ss = ax.get_subplotspec()
@@ -488,8 +488,8 @@ def match_submerged_margins(layoutgrids, fig):
488488
for sfig in fig.subfigs:
489489
match_submerged_margins(layoutgrids, sfig)
490490

491-
axs = [a for a in fig.get_axes() if (hasattr(a, 'get_subplotspec')
492-
and a.get_in_layout())]
491+
axs = [a for a in fig.get_axes()
492+
if a.get_subplotspec() and a.get_in_layout()]
493493

494494
for ax1 in axs:
495495
ss1 = ax1.get_subplotspec()
@@ -620,7 +620,7 @@ def reposition_axes(layoutgrids, fig, renderer, *,
620620
wspace=wspace, hspace=hspace)
621621

622622
for ax in fig._localaxes:
623-
if not hasattr(ax, 'get_subplotspec') or not ax.get_in_layout():
623+
if not ax.get_subplotspec() or not ax.get_in_layout():
624624
continue
625625

626626
# grid bbox is in Figure coordinates, but we specify in panel
@@ -742,7 +742,7 @@ def reset_margins(layoutgrids, fig):
742742
for sfig in fig.subfigs:
743743
reset_margins(layoutgrids, sfig)
744744
for ax in fig.axes:
745-
if hasattr(ax, 'get_subplotspec') and ax.get_in_layout():
745+
if ax.get_subplotspec() and ax.get_in_layout():
746746
ss = ax.get_subplotspec()
747747
gs = ss.get_gridspec()
748748
if gs in layoutgrids:

lib/matplotlib/axes/__init__.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,18 @@
1-
from ._subplots import *
1+
from . import _base
22
from ._axes import *
3+
4+
# Backcompat.
5+
from ._axes import Axes as Subplot
6+
7+
8+
class _SubplotBaseMeta(type):
9+
def __instancecheck__(self, obj):
10+
return (isinstance(obj, _base._AxesBase)
11+
and obj.get_subplotspec() is not None)
12+
13+
14+
class SubplotBase(metaclass=_SubplotBaseMeta):
15+
pass
16+
17+
18+
def subplot_class_factory(cls): return cls

lib/matplotlib/axes/_base.py

+117-18
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from collections.abc import MutableSequence
1+
from collections.abc import Iterable, MutableSequence
22
from contextlib import ExitStack
33
import functools
44
import inspect
@@ -18,6 +18,7 @@
1818
import matplotlib.collections as mcoll
1919
import matplotlib.colors as mcolors
2020
import matplotlib.font_manager as font_manager
21+
from matplotlib.gridspec import SubplotSpec
2122
import matplotlib.image as mimage
2223
import matplotlib.lines as mlines
2324
import matplotlib.patches as mpatches
@@ -569,8 +570,8 @@ def __str__(self):
569570
return "{0}({1[0]:g},{1[1]:g};{1[2]:g}x{1[3]:g})".format(
570571
type(self).__name__, self._position.bounds)
571572

572-
def __init__(self, fig, rect,
573-
*,
573+
def __init__(self, fig,
574+
*args,
574575
facecolor=None, # defaults to rc axes.facecolor
575576
frameon=True,
576577
sharex=None, # use Axes instance's xaxis info
@@ -589,9 +590,18 @@ def __init__(self, fig, rect,
589590
fig : `~matplotlib.figure.Figure`
590591
The Axes is built in the `.Figure` *fig*.
591592
592-
rect : tuple (left, bottom, width, height).
593-
The Axes is built in the rectangle *rect*. *rect* is in
594-
`.Figure` coordinates.
593+
*args
594+
``*args`` can be a single ``(left, bottom, width, height)``
595+
rectangle or a single `.Bbox`. This specifies the rectangle (in
596+
figure coordinates) where the Axes is positioned.
597+
598+
``*args`` can also consist of three numbers or a single three-digit
599+
number; in the latter case, the digits are considered as
600+
independent numbers. The numbers are interpreted as ``(nrows,
601+
ncols, index)``: ``(nrows, ncols)`` specifies the size of an array
602+
of subplots, and ``index`` is the 1-based index of the subplot
603+
being created. Finally, ``*args`` can also directly be a
604+
`.SubplotSpec` instance.
595605
596606
sharex, sharey : `~.axes.Axes`, optional
597607
The x or y `~.matplotlib.axis` is shared with the x or
@@ -616,10 +626,21 @@ def __init__(self, fig, rect,
616626
"""
617627

618628
super().__init__()
619-
if isinstance(rect, mtransforms.Bbox):
620-
self._position = rect
629+
if "rect" in kwargs:
630+
if args:
631+
raise TypeError(
632+
"'rect' cannot be used together with positional arguments")
633+
rect = kwargs.pop("rect")
634+
_api.check_isinstance((mtransforms.Bbox, Iterable), rect=rect)
635+
args = (rect,)
636+
self._subplotspec = subplotspec = None
637+
if len(args) == 1 and isinstance(args[0], mtransforms.Bbox):
638+
self._position = args[0]
639+
elif len(args) == 1 and np.iterable(args[0]):
640+
self._position = mtransforms.Bbox.from_bounds(*args[0])
621641
else:
622-
self._position = mtransforms.Bbox.from_bounds(*rect)
642+
self._position = self._originalPosition = mtransforms.Bbox.unit()
643+
subplotspec = SubplotSpec._from_subplot_args(fig, args)
623644
if self._position.width < 0 or self._position.height < 0:
624645
raise ValueError('Width and height specified must be non-negative')
625646
self._originalPosition = self._position.frozen()
@@ -632,8 +653,14 @@ def __init__(self, fig, rect,
632653
self._sharey = sharey
633654
self.set_label(label)
634655
self.set_figure(fig)
656+
# The subplotspec needs to be set after the figure (so that
657+
# figure-level subplotpars are taken into account), but the figure
658+
# needs to be set after self._position is initialized.
659+
if subplotspec:
660+
self.set_subplotspec(subplotspec)
635661
self.set_box_aspect(box_aspect)
636662
self._axes_locator = None # Optionally set via update(kwargs).
663+
637664
# placeholder for any colorbars added that use this Axes.
638665
# (see colorbar.py):
639666
self._colorbars = []
@@ -737,6 +764,19 @@ def __repr__(self):
737764
fields += [f"{name}label={axis.get_label().get_text()!r}"]
738765
return f"<{self.__class__.__name__}: " + ", ".join(fields) + ">"
739766

767+
def get_subplotspec(self):
768+
"""Return the `.SubplotSpec` associated with the subplot, or None."""
769+
return self._subplotspec
770+
771+
def set_subplotspec(self, subplotspec):
772+
"""Set the `.SubplotSpec`. associated with the subplot."""
773+
self._subplotspec = subplotspec
774+
self._set_position(subplotspec.get_position(self.figure))
775+
776+
def get_gridspec(self):
777+
"""Return the `.GridSpec` associated with the subplot, or None."""
778+
return self._subplotspec.get_gridspec() if self._subplotspec else None
779+
740780
@_api.delete_parameter("3.6", "args")
741781
@_api.delete_parameter("3.6", "kwargs")
742782
def get_window_extent(self, renderer=None, *args, **kwargs):
@@ -4424,17 +4464,23 @@ def get_tightbbox(self, renderer=None, call_axes_locator=True,
44244464

44254465
def _make_twin_axes(self, *args, **kwargs):
44264466
"""Make a twinx Axes of self. This is used for twinx and twiny."""
4427-
# Typically, SubplotBase._make_twin_axes is called instead of this.
44284467
if 'sharex' in kwargs and 'sharey' in kwargs:
4429-
raise ValueError("Twinned Axes may share only one axis")
4430-
ax2 = self.figure.add_axes(
4431-
self.get_position(True), *args, **kwargs,
4432-
axes_locator=_TransformedBoundsLocator(
4433-
[0, 0, 1, 1], self.transAxes))
4468+
# The following line is added in v2.2 to avoid breaking Seaborn,
4469+
# which currently uses this internal API.
4470+
if kwargs["sharex"] is not self and kwargs["sharey"] is not self:
4471+
raise ValueError("Twinned Axes may share only one axis")
4472+
ss = self.get_subplotspec()
4473+
if ss:
4474+
twin = self.figure.add_subplot(ss, *args, **kwargs)
4475+
else:
4476+
twin = self.figure.add_axes(
4477+
self.get_position(True), *args, **kwargs,
4478+
axes_locator=_TransformedBoundsLocator(
4479+
[0, 0, 1, 1], self.transAxes))
44344480
self.set_adjustable('datalim')
4435-
ax2.set_adjustable('datalim')
4436-
self._twinned_axes.join(self, ax2)
4437-
return ax2
4481+
twin.set_adjustable('datalim')
4482+
self._twinned_axes.join(self, twin)
4483+
return twin
44384484

44394485
def twinx(self):
44404486
"""
@@ -4502,3 +4548,56 @@ def get_shared_x_axes(self):
45024548
def get_shared_y_axes(self):
45034549
"""Return an immutable view on the shared y-axes Grouper."""
45044550
return cbook.GrouperView(self._shared_axes["y"])
4551+
4552+
def label_outer(self):
4553+
"""
4554+
Only show "outer" labels and tick labels.
4555+
4556+
x-labels are only kept for subplots on the last row (or first row, if
4557+
labels are on the top side); y-labels only for subplots on the first
4558+
column (or last column, if labels are on the right side).
4559+
"""
4560+
self._label_outer_xaxis(check_patch=False)
4561+
self._label_outer_yaxis(check_patch=False)
4562+
4563+
def _label_outer_xaxis(self, *, check_patch):
4564+
# see documentation in label_outer.
4565+
if check_patch and not isinstance(self.patch, mpl.patches.Rectangle):
4566+
return
4567+
ss = self.get_subplotspec()
4568+
if not ss:
4569+
return
4570+
label_position = self.xaxis.get_label_position()
4571+
if not ss.is_first_row(): # Remove top label/ticklabels/offsettext.
4572+
if label_position == "top":
4573+
self.set_xlabel("")
4574+
self.xaxis.set_tick_params(which="both", labeltop=False)
4575+
if self.xaxis.offsetText.get_position()[1] == 1:
4576+
self.xaxis.offsetText.set_visible(False)
4577+
if not ss.is_last_row(): # Remove bottom label/ticklabels/offsettext.
4578+
if label_position == "bottom":
4579+
self.set_xlabel("")
4580+
self.xaxis.set_tick_params(which="both", labelbottom=False)
4581+
if self.xaxis.offsetText.get_position()[1] == 0:
4582+
self.xaxis.offsetText.set_visible(False)
4583+
4584+
def _label_outer_yaxis(self, *, check_patch):
4585+
# see documentation in label_outer.
4586+
if check_patch and not isinstance(self.patch, mpl.patches.Rectangle):
4587+
return
4588+
ss = self.get_subplotspec()
4589+
if not ss:
4590+
return
4591+
label_position = self.yaxis.get_label_position()
4592+
if not ss.is_first_col(): # Remove left label/ticklabels/offsettext.
4593+
if label_position == "left":
4594+
self.set_ylabel("")
4595+
self.yaxis.set_tick_params(which="both", labelleft=False)
4596+
if self.yaxis.offsetText.get_position()[0] == 0:
4597+
self.yaxis.offsetText.set_visible(False)
4598+
if not ss.is_last_col(): # Remove right label/ticklabels/offsettext.
4599+
if label_position == "right":
4600+
self.set_ylabel("")
4601+
self.yaxis.set_tick_params(which="both", labelright=False)
4602+
if self.yaxis.offsetText.get_position()[0] == 1:
4603+
self.yaxis.offsetText.set_visible(False)

0 commit comments

Comments
 (0)