256 lines
10 KiB
Python
256 lines
10 KiB
Python
|
import functools
|
||
|
import uuid
|
||
|
|
||
|
from matplotlib import cbook, docstring
|
||
|
import matplotlib.artist as martist
|
||
|
from matplotlib.axes._axes import Axes
|
||
|
from matplotlib.gridspec import GridSpec, SubplotSpec
|
||
|
import matplotlib._layoutbox as layoutbox
|
||
|
|
||
|
|
||
|
class SubplotBase:
|
||
|
"""
|
||
|
Base class for subplots, which are :class:`Axes` instances with
|
||
|
additional methods to facilitate generating and manipulating a set
|
||
|
of :class:`Axes` within a figure.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, fig, *args, **kwargs):
|
||
|
"""
|
||
|
Parameters
|
||
|
----------
|
||
|
fig : `matplotlib.figure.Figure`
|
||
|
|
||
|
*args : tuple (*nrows*, *ncols*, *index*) or int
|
||
|
The array of subplots in the figure has dimensions ``(nrows,
|
||
|
ncols)``, and *index* is the index of the subplot being created.
|
||
|
*index* starts at 1 in the upper left corner and increases to the
|
||
|
right.
|
||
|
|
||
|
If *nrows*, *ncols*, and *index* are all single digit numbers, then
|
||
|
*args* can be passed as a single 3-digit number (e.g. 234 for
|
||
|
(2, 3, 4)).
|
||
|
"""
|
||
|
|
||
|
self.figure = fig
|
||
|
|
||
|
if len(args) == 1:
|
||
|
if isinstance(args[0], SubplotSpec):
|
||
|
self._subplotspec = args[0]
|
||
|
else:
|
||
|
try:
|
||
|
s = str(int(args[0]))
|
||
|
rows, cols, num = map(int, s)
|
||
|
except ValueError:
|
||
|
raise ValueError('Single argument to subplot must be '
|
||
|
'a 3-digit integer')
|
||
|
self._subplotspec = GridSpec(rows, cols,
|
||
|
figure=self.figure)[num - 1]
|
||
|
# num - 1 for converting from MATLAB to python indexing
|
||
|
elif len(args) == 3:
|
||
|
rows, cols, num = args
|
||
|
rows = int(rows)
|
||
|
cols = int(cols)
|
||
|
if rows <= 0:
|
||
|
raise ValueError(f'Number of rows must be > 0, not {rows}')
|
||
|
if cols <= 0:
|
||
|
raise ValueError(f'Number of columns must be > 0, not {cols}')
|
||
|
if isinstance(num, tuple) and len(num) == 2:
|
||
|
num = [int(n) for n in num]
|
||
|
self._subplotspec = GridSpec(
|
||
|
rows, cols,
|
||
|
figure=self.figure)[(num[0] - 1):num[1]]
|
||
|
else:
|
||
|
if num < 1 or num > rows*cols:
|
||
|
raise ValueError(
|
||
|
f"num must be 1 <= num <= {rows*cols}, not {num}")
|
||
|
self._subplotspec = GridSpec(
|
||
|
rows, cols, figure=self.figure)[int(num) - 1]
|
||
|
# num - 1 for converting from MATLAB to python indexing
|
||
|
else:
|
||
|
raise ValueError(f'Illegal argument(s) to subplot: {args}')
|
||
|
|
||
|
self.update_params()
|
||
|
|
||
|
# _axes_class is set in the subplot_class_factory
|
||
|
self._axes_class.__init__(self, fig, self.figbox, **kwargs)
|
||
|
# add a layout box to this, for both the full axis, and the poss
|
||
|
# of the axis. We need both because the axes may become smaller
|
||
|
# due to parasitic axes and hence no longer fill the subplotspec.
|
||
|
if self._subplotspec._layoutbox is None:
|
||
|
self._layoutbox = None
|
||
|
self._poslayoutbox = None
|
||
|
else:
|
||
|
name = self._subplotspec._layoutbox.name + '.ax'
|
||
|
name = name + layoutbox.seq_id()
|
||
|
self._layoutbox = layoutbox.LayoutBox(
|
||
|
parent=self._subplotspec._layoutbox,
|
||
|
name=name,
|
||
|
artist=self)
|
||
|
self._poslayoutbox = layoutbox.LayoutBox(
|
||
|
parent=self._layoutbox,
|
||
|
name=self._layoutbox.name+'.pos',
|
||
|
pos=True, subplot=True, artist=self)
|
||
|
|
||
|
def __reduce__(self):
|
||
|
# get the first axes class which does not inherit from a subplotbase
|
||
|
axes_class = next(
|
||
|
c for c in type(self).__mro__
|
||
|
if issubclass(c, Axes) and not issubclass(c, SubplotBase))
|
||
|
return (_picklable_subplot_class_constructor,
|
||
|
(axes_class,),
|
||
|
self.__getstate__())
|
||
|
|
||
|
def get_geometry(self):
|
||
|
"""Get the subplot geometry, e.g., (2, 2, 3)."""
|
||
|
rows, cols, num1, num2 = self.get_subplotspec().get_geometry()
|
||
|
return rows, cols, num1 + 1 # for compatibility
|
||
|
|
||
|
# COVERAGE NOTE: Never used internally or from examples
|
||
|
def change_geometry(self, numrows, numcols, num):
|
||
|
"""Change subplot geometry, e.g., from (1, 1, 1) to (2, 2, 3)."""
|
||
|
self._subplotspec = GridSpec(numrows, numcols,
|
||
|
figure=self.figure)[num - 1]
|
||
|
self.update_params()
|
||
|
self.set_position(self.figbox)
|
||
|
|
||
|
def get_subplotspec(self):
|
||
|
"""get the SubplotSpec instance associated with the subplot"""
|
||
|
return self._subplotspec
|
||
|
|
||
|
def set_subplotspec(self, subplotspec):
|
||
|
"""set the SubplotSpec instance associated with the subplot"""
|
||
|
self._subplotspec = subplotspec
|
||
|
|
||
|
def get_gridspec(self):
|
||
|
"""get the GridSpec instance associated with the subplot"""
|
||
|
return self._subplotspec.get_gridspec()
|
||
|
|
||
|
def update_params(self):
|
||
|
"""update the subplot position from fig.subplotpars"""
|
||
|
self.figbox, _, _, self.numRows, self.numCols = \
|
||
|
self.get_subplotspec().get_position(self.figure,
|
||
|
return_all=True)
|
||
|
|
||
|
@cbook.deprecated("3.2", alternative="ax.get_subplotspec().rowspan.start")
|
||
|
@property
|
||
|
def rowNum(self):
|
||
|
return self.get_subplotspec().rowspan.start
|
||
|
|
||
|
@cbook.deprecated("3.2", alternative="ax.get_subplotspec().colspan.start")
|
||
|
@property
|
||
|
def colNum(self):
|
||
|
return self.get_subplotspec().colspan.start
|
||
|
|
||
|
def is_first_row(self):
|
||
|
return self.get_subplotspec().rowspan.start == 0
|
||
|
|
||
|
def is_last_row(self):
|
||
|
return self.get_subplotspec().rowspan.stop == self.get_gridspec().nrows
|
||
|
|
||
|
def is_first_col(self):
|
||
|
return self.get_subplotspec().colspan.start == 0
|
||
|
|
||
|
def is_last_col(self):
|
||
|
return self.get_subplotspec().colspan.stop == self.get_gridspec().ncols
|
||
|
|
||
|
def label_outer(self):
|
||
|
"""
|
||
|
Only show "outer" labels and tick labels.
|
||
|
|
||
|
x-labels are only kept for subplots on the last row; y-labels only for
|
||
|
subplots on the first column.
|
||
|
"""
|
||
|
lastrow = self.is_last_row()
|
||
|
firstcol = self.is_first_col()
|
||
|
if not lastrow:
|
||
|
for label in self.get_xticklabels(which="both"):
|
||
|
label.set_visible(False)
|
||
|
self.get_xaxis().get_offset_text().set_visible(False)
|
||
|
self.set_xlabel("")
|
||
|
if not firstcol:
|
||
|
for label in self.get_yticklabels(which="both"):
|
||
|
label.set_visible(False)
|
||
|
self.get_yaxis().get_offset_text().set_visible(False)
|
||
|
self.set_ylabel("")
|
||
|
|
||
|
def _make_twin_axes(self, *args, **kwargs):
|
||
|
"""Make a twinx axes of self. This is used for twinx and twiny."""
|
||
|
if 'sharex' in kwargs and 'sharey' in kwargs:
|
||
|
# 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")
|
||
|
# The dance here with label is to force add_subplot() to create a new
|
||
|
# Axes (by passing in a label never seen before). Note that this does
|
||
|
# not affect plot reactivation by subplot() as twin axes can never be
|
||
|
# reactivated by subplot().
|
||
|
sentinel = str(uuid.uuid4())
|
||
|
real_label = kwargs.pop("label", sentinel)
|
||
|
twin = self.figure.add_subplot(
|
||
|
self.get_subplotspec(), *args, label=sentinel, **kwargs)
|
||
|
if real_label is not sentinel:
|
||
|
twin.set_label(real_label)
|
||
|
self.set_adjustable('datalim')
|
||
|
twin.set_adjustable('datalim')
|
||
|
if self._layoutbox is not None and twin._layoutbox is not None:
|
||
|
# make the layout boxes be explicitly the same
|
||
|
twin._layoutbox.constrain_same(self._layoutbox)
|
||
|
twin._poslayoutbox.constrain_same(self._poslayoutbox)
|
||
|
self._twinned_axes.join(self, twin)
|
||
|
return twin
|
||
|
|
||
|
|
||
|
# this here to support cartopy which was using a private part of the
|
||
|
# API to register their Axes subclasses.
|
||
|
|
||
|
# In 3.1 this should be changed to a dict subclass that warns on use
|
||
|
# In 3.3 to a dict subclass that raises a useful exception on use
|
||
|
# In 3.4 should be removed
|
||
|
|
||
|
# The slow timeline is to give cartopy enough time to get several
|
||
|
# release out before we break them.
|
||
|
_subplot_classes = {}
|
||
|
|
||
|
|
||
|
@functools.lru_cache(None)
|
||
|
def subplot_class_factory(axes_class=None):
|
||
|
"""
|
||
|
This makes a new class that inherits from `.SubplotBase` and the
|
||
|
given axes_class (which is assumed to be a subclass of `.axes.Axes`).
|
||
|
This is perhaps a little bit roundabout to make a new class on
|
||
|
the fly like this, but it means that a new Subplot class does
|
||
|
not have to be created for every type of Axes.
|
||
|
"""
|
||
|
if axes_class is None:
|
||
|
axes_class = Axes
|
||
|
try:
|
||
|
# Avoid creating two different instances of GeoAxesSubplot...
|
||
|
# Only a temporary backcompat fix. This should be removed in
|
||
|
# 3.4
|
||
|
return next(cls for cls in SubplotBase.__subclasses__()
|
||
|
if cls.__bases__ == (SubplotBase, axes_class))
|
||
|
except StopIteration:
|
||
|
return type("%sSubplot" % axes_class.__name__,
|
||
|
(SubplotBase, axes_class),
|
||
|
{'_axes_class': axes_class})
|
||
|
|
||
|
|
||
|
# This is provided for backward compatibility
|
||
|
Subplot = subplot_class_factory()
|
||
|
|
||
|
|
||
|
def _picklable_subplot_class_constructor(axes_class):
|
||
|
"""
|
||
|
This stub class exists to return the appropriate subplot class when called
|
||
|
with an axes class. This is purely to allow pickling of Axes and Subplots.
|
||
|
"""
|
||
|
subplot_class = subplot_class_factory(axes_class)
|
||
|
return subplot_class.__new__(subplot_class)
|
||
|
|
||
|
|
||
|
docstring.interpd.update(Axes=martist.kwdoc(Axes))
|
||
|
docstring.dedent_interpd(Axes.__init__)
|
||
|
|
||
|
docstring.interpd.update(Subplot=martist.kwdoc(Axes))
|