Skip to content

Commit 4b8b971

Browse files
committed
Merge SubplotBase into AxesBase.
1 parent 9b1fcf6 commit 4b8b971

27 files changed

+264
-314
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,7 @@
1+
All Axes have ``get_subplotspec`` and ``get_gridspec`` methods 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``. For compatibility with older
6+
Matplotlib releases, one can also check
7+
``hasattr(ax, "get_gridspec") and 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

+18-21
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,8 @@ 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'):
191-
gs = ax.get_subplotspec().get_gridspec()
190+
gs = ax.get_gridspec()
191+
if gs is not None:
192192
layoutgrids = make_layoutgrids_gs(layoutgrids, gs)
193193

194194
return layoutgrids
@@ -248,24 +248,22 @@ def check_no_collapsed_axes(layoutgrids, fig):
248248
ok = check_no_collapsed_axes(layoutgrids, sfig)
249249
if not ok:
250250
return False
251-
252251
for ax in fig.axes:
253-
if hasattr(ax, 'get_subplotspec'):
254-
gs = ax.get_subplotspec().get_gridspec()
255-
if gs in layoutgrids:
256-
lg = layoutgrids[gs]
257-
for i in range(gs.nrows):
258-
for j in range(gs.ncols):
259-
bb = lg.get_inner_bbox(i, j)
260-
if bb.width <= 0 or bb.height <= 0:
261-
return False
252+
gs = ax.get_gridspec()
253+
if gs in layoutgrids: # also implies gs is not None.
254+
lg = layoutgrids[gs]
255+
for i in range(gs.nrows):
256+
for j in range(gs.ncols):
257+
bb = lg.get_inner_bbox(i, j)
258+
if bb.width <= 0 or bb.height <= 0:
259+
return False
262260
return True
263261

264262

265263
def compress_fixed_aspect(layoutgrids, fig):
266264
gs = None
267265
for ax in fig.axes:
268-
if not hasattr(ax, 'get_subplotspec'):
266+
if ax.get_subplotspec() is None:
269267
continue
270268
ax.apply_aspect()
271269
sub = ax.get_subplotspec()
@@ -357,7 +355,7 @@ def make_layout_margins(layoutgrids, fig, renderer, *, w_pad=0, h_pad=0,
357355
layoutgrids[sfig].parent.edit_outer_margin_mins(margins, ss)
358356

359357
for ax in fig._localaxes:
360-
if not hasattr(ax, 'get_subplotspec') or not ax.get_in_layout():
358+
if not ax.get_subplotspec() or not ax.get_in_layout():
361359
continue
362360

363361
ss = ax.get_subplotspec()
@@ -488,8 +486,8 @@ def match_submerged_margins(layoutgrids, fig):
488486
for sfig in fig.subfigs:
489487
match_submerged_margins(layoutgrids, sfig)
490488

491-
axs = [a for a in fig.get_axes() if (hasattr(a, 'get_subplotspec')
492-
and a.get_in_layout())]
489+
axs = [a for a in fig.get_axes()
490+
if a.get_subplotspec() is not None and a.get_in_layout()]
493491

494492
for ax1 in axs:
495493
ss1 = ax1.get_subplotspec()
@@ -620,7 +618,7 @@ def reposition_axes(layoutgrids, fig, renderer, *,
620618
wspace=wspace, hspace=hspace)
621619

622620
for ax in fig._localaxes:
623-
if not hasattr(ax, 'get_subplotspec') or not ax.get_in_layout():
621+
if ax.get_subplotspec() is None or not ax.get_in_layout():
624622
continue
625623

626624
# grid bbox is in Figure coordinates, but we specify in panel
@@ -742,10 +740,9 @@ def reset_margins(layoutgrids, fig):
742740
for sfig in fig.subfigs:
743741
reset_margins(layoutgrids, sfig)
744742
for ax in fig.axes:
745-
if hasattr(ax, 'get_subplotspec') and ax.get_in_layout():
746-
ss = ax.get_subplotspec()
747-
gs = ss.get_gridspec()
748-
if gs in layoutgrids:
743+
if ax.get_in_layout():
744+
gs = ax.get_gridspec()
745+
if gs in layoutgrids: # also implies gs is not None.
749746
layoutgrids[gs].reset_margins()
750747
layoutgrids[fig].reset_margins()
751748

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

+119-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+
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,16 @@ 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)
661+
else:
662+
self._subplotspec = None
635663
self.set_box_aspect(box_aspect)
636664
self._axes_locator = None # Optionally set via update(kwargs).
665+
637666
# placeholder for any colorbars added that use this Axes.
638667
# (see colorbar.py):
639668
self._colorbars = []
@@ -737,6 +766,19 @@ def __repr__(self):
737766
fields += [f"{name}label={axis.get_label().get_text()!r}"]
738767
return f"<{self.__class__.__name__}: " + ", ".join(fields) + ">"
739768

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

44254467
def _make_twin_axes(self, *args, **kwargs):
44264468
"""Make a twinx Axes of self. This is used for twinx and twiny."""
4427-
# Typically, SubplotBase._make_twin_axes is called instead of this.
44284469
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))
4470+
# The following line is added in v2.2 to avoid breaking Seaborn,
4471+
# which currently uses this internal API.
4472+
if kwargs["sharex"] is not self and kwargs["sharey"] is not self:
4473+
raise ValueError("Twinned Axes may share only one axis")
4474+
ss = self.get_subplotspec()
4475+
if ss:
4476+
twin = self.figure.add_subplot(ss, *args, **kwargs)
4477+
else:
4478+
twin = self.figure.add_axes(
4479+
self.get_position(True), *args, **kwargs,
4480+
axes_locator=_TransformedBoundsLocator(
4481+
[0, 0, 1, 1], self.transAxes))
44344482
self.set_adjustable('datalim')
4435-
ax2.set_adjustable('datalim')
4436-
self._twinned_axes.join(self, ax2)
4437-
return ax2
4483+
twin.set_adjustable('datalim')
4484+
self._twinned_axes.join(self, twin)
4485+
return twin
44384486

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

0 commit comments

Comments
 (0)