-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
---|---|---|
@@ -1,2 +1,18 @@ | ||
from ._subplots import * | ||
from . import _base | ||
from ._axes import * | ||
|
||
# Backcompat. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
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 | ||
|
@@ -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 | ||
|
@@ -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 | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 But that may be for later. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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() | ||
|
@@ -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 = [] | ||
|
@@ -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): | ||
|
@@ -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): | ||
""" | ||
|
@@ -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) |
There was a problem hiding this comment.
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?There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, #24236