123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255 |
- 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))
|