8000 Merge pull request #23573 from anntzer/nosubplotbase · matplotlib/matplotlib@990a1de · GitHub
[go: up one dir, main page]

Skip to content

Commit 990a1de

Browse files
authored
Merge pull request #23573 from anntzer/nosubplotbase
Merge SubplotBase into AxesBase.
2 parents e761485 + c73f4c4 commit 990a1de

File tree

27 files changed

+258
-314
lines changed

27 files changed

+258
-314
lines changed

doc/api/axes_api.rst

Lines changed: 4 additions & 12 deletions
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
Lines changed: 7 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 3 additions & 3 deletions
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

Lines changed: 18 additions & 21 deletions
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

Lines changed: 17 additions & 1 deletion
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

Lines changed: 119 additions & 18 deletions
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
@@ -571,8 +572,8 @@ def __str__(self):
571572
return "{0}({1[0]:g},{1[1]:g};{1[2]:g}x{1[3]:g})".format(
572573
type(self).__name__, self._position.bounds)
573574

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

620630
super().__init__()
621-
if isinstance(rect, mtransforms.Bbox):
622-
self._position = rect
631+
if "rect" in kwargs:
632+
if args:
633+
raise TypeError(
634+
"'rect' cannot be used together with positional arguments")
635+
rect = kwargs.pop("rect")
636+
_api.check_isinstance((mtransforms.Bbox, Iterable), rect=rect)
637+
args = (rect,)
638+
subplotspec = None
639+
if len(args) == 1 and isinstance(args[0], mtransforms.Bbox):
640+
self._position = args[0]
641+
elif len(args) == 1 and np.iterable(args[0]):
642+
self._position = mtransforms.Bbox.from_bounds(*args[0])
623643
else:
624-
self._position = mtransforms.Bbox.from_bounds(*rect)
644+
self._position = self._originalPosition = mtransforms.Bbox.unit()
645+
subplotspec = SubplotSpec._from_subplot_args(fig, args)
625646
if self._position.width < 0 or self._position.height < 0:
626647
raise ValueError('Width and height specified must be non-negative')
627648
self._originalPosition = self._position.frozen()
@@ -634,8 +655,16 @@ def __init__(self, fig, rect,
634655
self._sharey = sharey
635656
self.set_label(label)
636657
self.set_figure(fig)
658+
# The subplotspec needs to be set after the figure (so that
659+
# figure-level subplotpars are taken into account), but the figure
660+
# needs to be set after self._position is initialized.
661+
if subplotspec:
662+
self.set_subplotspec(subplotspec)
663+
else:
664+
self._subplotspec = None
637665
self.set_box_aspect(box_aspect)
638666
self._axes_locator = None # Optionally set via update(kwargs).
667+
639668
# placeholder for any colorbars added that use this Axes.
640669
# (see colorbar.py):
641670
self._colorbars = []
@@ -753,6 +782,19 @@ def __repr__(self):
753782
fields += [f"{name}label={axis.get_label().get_text()!r}"]
754783
return f"<{self.__class__.__name__}: " + ", ".join(fields) + ">"
755784

785+
def get_subplotspec(self):
786+
"""Return the `.SubplotSpec` associated with the subplot, or None."""
787+
return self._subplotspec
788+
789+
def set_subplotspec(self, subplotspec):
790+
"""Set the `.SubplotSpec`. associated with the subplot."""
791+
self._subplotspec = subplotspec
792+
self._set_position(subplotspec.get_position(self.figure))
793+
794+
def get_gridspec(self):
795+
"""Return the `.GridSpec` associated with the subplot, or None."""
796+
return self._subplotspec.get_gridspec() if self._subplotspec else None
797+
756798
@_api.delete_parameter("3.6", "args")
757799
@_api.delete_parameter("3.6", "kwargs")
758800
def get_window_extent(self, renderer=None, *args, **kwargs):
@@ -4460,17 +4502,23 @@ def get_tightbbox(self, renderer=None, call_axes_locator=True,
44604502

44614503
def _make_twin_axes(self, *args, **kwargs):
44624504
"""Make a twinx Axes of self. This is used for twinx and twiny."""
4463-
# Typically, SubplotBase._make_twin_axes is called instead of this.
44644505
if 'sharex' in kwargs and 'sharey' in kwargs:
4465-
raise ValueError("Twinned Axes may share only one axis")
4466-
ax2 = self.figure.add_axes(
4467-
self.get_position(True), *args, **kwargs,
4468-
axes_locator=_TransformedBoundsLocator(
4469-
[0, 0, 1, 1], self.transAxes))
4506+
# The following line is added in v2.2 to avoid breaking Seaborn,
4507+
# which currently uses this internal API.
4508+
if kwargs["sharex"] is not self and kwargs["sharey"] is not self:
4509+
raise ValueError("Twinned Axes may share only one axis")
4510+
ss = self.get_subplotspec()
4511+
if ss:
4512+
twin = self.figure.add_subplot(ss, *args, **kwargs)
4513+
else:
4514+
twin = self.figure.add_axes(
4515+
self.get_position(True), *args, **kwargs,
4516+
axes_locator=_TransformedBoundsLocator(
4517+
[0, 0, 1, 1], self.transAxes))
44704518
self.set_adjustable('datalim')
4471-
ax2.set_adjustable('datalim')
4472-
self._twinned_axes.join(self, ax2)
4473-
return ax2
4519+
twin.set_adjustable('datalim')
4520+
self._twinned_axes.join(self, twin)
4521+
return twin
44744522

44754523
def twinx(self):
44764524
"""
@@ -4538,3 +4586,56 @@ def get_shared_x_axes(self):
45384586
def get_shared_y_axes(self):
45394587
"""Return an immutable view on the shared y-axes Grouper."""
45404588
return cbook.GrouperView(self._shared_axes["y"])
4589+
4590+
def label_outer(self):
4591+
"""
4592+
Only show "outer" labels and tick labels.
4593+
4594+
x-labels are only kept for subplots on the last row (or first row, if
4595+
labels are on the top side); y-labels only for subplots on the first
4596+
column (or last column, if labels are on the right side).
4597+
"""
4598+
self._label_outer_xaxis(check_patch=False)
4599+
self._label_outer_yaxis(check_patch=False)
4600+
4601+
def _label_outer_xaxis(self, *, check_patch):
4602+
# see documentation in label_outer.
4603+
if check_patch and not isinstance(self.patch, mpl.patches.Rectangle):
4604+
return
4605+
ss = self.get_subplotspec()
4606+
if not ss:
4607+
return
4608+
label_position = self.xaxis.get_label_position()
4609+
if not ss.is_first_row(): # Remove top label/ticklabels/offsettext.
4610+
if label_position == "top":
4611+
self.set_xlabel("")
4612+
self.xaxis.set_tick_params(which="both", labeltop=False)
4613+
if self.xaxis.offsetText.get_position()[1] == 1:
4614+
self.xaxis.offsetText.set_visible(False)
4615+
if not ss.is_last_row(): # Remove bottom label/ticklabels/offsettext.
4616+
if label_position == "bottom":
4617+
self.set_xlabel("")
4618+
self.xaxis.set_tick_params(which="both", labelbottom=False)
4619+
if self.xaxis.offsetText.get_position()[1] == 0:
4620+
self.xaxis.offsetText.set_visible(False)
4621+
4622+
def _label_outer_yaxis(self, *, check_patch):
4623+
# see documentation in label_outer.
4624+
if check_patch and not isinstance(self.patch, mpl.patches.Rectangle):
4625+
return
4626+
ss = self.get_subplotspec()
4627+
if not ss:
4628+
return
4629+
label_position = self.yaxis.get_label_position()
4630+
if not ss.is_first_col(): # Remove left label/ticklabels/offsettext.
4631+
if label_position == "left":
4632+
self.set_ylabel("")
4633+
self.yaxis.set_tick_params(which="both", labelleft=False)
4634+
if self.yaxis.offsetText.get_position()[0] == 0:
4635+
self.yaxis.offsetText.set_visible(False)
4636+
if not ss.is_last_col(): # Remove right label/ticklabels/offsettext.
4637+
if label_position == "right":
4638+
self.set_ylabel("")
4639+
self.yaxis.set_tick_params(which="both", labelright=False)
4640+
if self.yaxis.offsetText.get_position()[0] == 1:
4641+
self.yaxis.offsetText.set_visible(False)

0 commit comments

Comments
 (0)
0