From 261f7062860df88d08451d810d1036853390673d Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Fri, 11 Sep 2020 00:12:02 +0200 Subject: [PATCH] Prepare for merging SubplotBase into AxesBase. It should be possible to merge SubplotBase into AxesBase, with all Axes having a `get_subplotspec()` method -- it's just that that method would return None for non-gridspec-positioned Axes. The advantage of doing so is that we could get rid of the rather hackish subplot_class_factory, and the dynamically generated AxesSubplot class, which appears nowhere in the docs. - Deprecate most Subplot-specific API: while it's fine for all axes to have a `get_subplotspec` which may return None, it would be a bit weird if they all also have e.g. a `is_last_row` for which it's not clear what value to return for non-gridspec-positioned Axes. Moving that to the SubplotSpec seems natural enough (and these are pretty low-level anyways). - Make most parameters to AxesBase keyword-only, so that we can later overload the positional parameters to be either a rect or a subplot triplet (which should ideally be passed packed as a single parameter rather than unpacked, but at least during the deprecation period it would be a pain to differentiate whether, in `Axes(fig, a, b, c)`, `b` was intended to be the second index of a subplot triplet or a `facecolor`...) Due to the order of calls during initialization, Axes3D self-adding to the figure was problematic. This is already getting removed in another PR, so I included the same change here without API changes notes just to get the tests to pass. However I can put a note in for this PR if it ends up being ready for merge first. --- .../next_api_changes/behavior/18564-AL.rst | 8 ++++ .../deprecations/18564-AL.rst | 16 +++++++ examples/userdemo/demo_gridspec06.py | 9 ++-- lib/matplotlib/axes/_base.py | 1 + lib/matplotlib/axes/_subplots.py | 45 ++++++++++++++----- lib/matplotlib/colorbar.py | 2 - lib/matplotlib/figure.py | 7 ++- lib/matplotlib/gridspec.py | 17 ++++++- lib/matplotlib/tests/test_collections.py | 2 +- lib/matplotlib/tests/test_colorbar.py | 6 +-- lib/matplotlib/tests/test_figure.py | 2 +- lib/mpl_toolkits/mplot3d/axes3d.py | 2 - lib/mpl_toolkits/tests/test_mplot3d.py | 2 +- tutorials/intermediate/gridspec.py | 9 ++-- 14 files changed, 93 insertions(+), 35 deletions(-) create mode 100644 doc/api/next_api_changes/behavior/18564-AL.rst create mode 100644 doc/api/next_api_changes/deprecations/18564-AL.rst diff --git a/doc/api/next_api_changes/behavior/18564-AL.rst b/doc/api/next_api_changes/behavior/18564-AL.rst new file mode 100644 index 000000000000..15617667fce0 --- /dev/null +++ b/doc/api/next_api_changes/behavior/18564-AL.rst @@ -0,0 +1,8 @@ +Axes3D no longer adds itself to figure +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +New `.Axes3D` objects previously added themselves to figures when they were +created, which lead to them being added twice if +``fig.add_subplot(111, projection='3d')`` was called. Now ``ax = Axes3d(fig)`` +will need to be explicitly added to the figure with ``fig.add_axes(ax)``, as +also needs to be done for normal `.axes.Axes`. diff --git a/doc/api/next_api_changes/deprecations/18564-AL.rst b/doc/api/next_api_changes/deprecations/18564-AL.rst new file mode 100644 index 000000000000..e5314e54bb3b --- /dev/null +++ b/doc/api/next_api_changes/deprecations/18564-AL.rst @@ -0,0 +1,16 @@ +Subplot-related attributes and methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Some ``SubplotBase`` attributes have been deprecated and/or moved to +`.SubplotSpec`: ``get_geometry`` (use `.SubplotBase.get_subplotspec` +instead), ``change_geometry`` (use `.SubplotBase.set_subplotspec` instead), +``is_first_row``, ``is_last_row``, ``is_first_col``, ``is_last_col`` (use the +corresponding methods on the `.SubplotSpec` instance instead), ``figbox`` (use +``ax.get_subplotspec().get_geometry(ax.figure)`` instead to recompute the +geometry, or ``ax.get_position()`` to read its current value), ``numRows``, +``numCols`` (use the ``nrows`` and ``ncols`` attribute on the `.GridSpec` +instead). + +Axes constructor +~~~~~~~~~~~~~~~~ +Parameters of the Axes constructor other than *fig* and *rect* will become +keyword-only in a future version. diff --git a/examples/userdemo/demo_gridspec06.py b/examples/userdemo/demo_gridspec06.py index 29830a1f20ef..80138b3d651c 100644 --- a/examples/userdemo/demo_gridspec06.py +++ b/examples/userdemo/demo_gridspec06.py @@ -29,9 +29,10 @@ def squiggle_xy(a, b, c, d): # show only the outside spines for ax in fig.get_axes(): - ax.spines['top'].set_visible(ax.is_first_row()) - ax.spines['bottom'].set_visible(ax.is_last_row()) - ax.spines['left'].set_visible(ax.is_first_col()) - ax.spines['right'].set_visible(ax.is_last_col()) + ss = ax.get_subplotspec() + ax.spines['top'].set_visible(ss.is_first_row()) + ax.spines['bottom'].set_visible(ss.is_last_row()) + ax.spines['left'].set_visible(ss.is_first_col()) + ax.spines['right'].set_visible(ss.is_last_col()) plt.show() diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 652dc29f3354..2b03167218fd 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -460,6 +460,7 @@ 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) + @cbook._make_keyword_only("3.4", "facecolor") def __init__(self, fig, rect, facecolor=None, # defaults to rc axes.facecolor frameon=True, diff --git a/lib/matplotlib/axes/_subplots.py b/lib/matplotlib/axes/_subplots.py index f7172a63dd5f..750fbaff2577 100644 --- a/lib/matplotlib/axes/_subplots.py +++ b/lib/matplotlib/axes/_subplots.py @@ -33,12 +33,10 @@ def __init__(self, fig, *args, **kwargs): **kwargs Keyword arguments are passed to the Axes (sub)class constructor. """ - - self.figure = fig - self._subplotspec = SubplotSpec._from_subplot_args(fig, args) - self.update_params() # _axes_class is set in the subplot_class_factory - self._axes_class.__init__(self, fig, self.figbox, **kwargs) + self._axes_class.__init__(self, fig, [0, 0, 1, 1], **kwargs) + # This will also update the axes position. + self.set_subplotspec(SubplotSpec._from_subplot_args(fig, args)) def __reduce__(self): # get the first axes class which does not inherit from a subplotbase @@ -49,12 +47,15 @@ def __reduce__(self): (axes_class,), self.__getstate__()) + @cbook.deprecated( + "3.4", alternative="get_subplotspec", + addendum="(get_subplotspec returns a SubplotSpec instance.)") 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 + @cbook.deprecated("3.4", alternative="set_subplotspec") 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, @@ -69,16 +70,33 @@ def get_subplotspec(self): def set_subplotspec(self, subplotspec): """Set the `.SubplotSpec`. instance associated with the subplot.""" self._subplotspec = subplotspec + self._set_position(subplotspec.get_position(self.figure)) def get_gridspec(self): """Return the `.GridSpec` instance associated with the subplot.""" return self._subplotspec.get_gridspec() + @cbook.deprecated( + "3.4", alternative="get_subplotspec().get_position(self.figure)") + @property + def figbox(self): + return self.get_subplotspec().get_position(self.figure) + + @cbook.deprecated("3.4", alternative="get_gridspec().nrows") + @property + def numRows(self): + return self.get_gridspec().nrows + + @cbook.deprecated("3.4", alternative="get_gridspec().ncols") + @property + def numCols(self): + return self.get_gridspec().ncols + + @cbook.deprecated("3.4") def update_params(self): """Update the subplot position from ``self.figure.subplotpars``.""" - self.figbox, _, _, self.numRows, self.numCols = \ - self.get_subplotspec().get_position(self.figure, - return_all=True) + # Now a no-op, as figbox/numRows/numCols are (deprecated) auto-updating + # properties. @cbook.deprecated("3.2", alternative="ax.get_subplotspec().rowspan.start") @property @@ -90,15 +108,19 @@ def rowNum(self): def colNum(self): return self.get_subplotspec().colspan.start + @cbook.deprecated("3.4", alternative="ax.get_subplotspec().is_first_row()") def is_first_row(self): return self.get_subplotspec().rowspan.start == 0 + @cbook.deprecated("3.4", alternative="ax.get_subplotspec().is_last_row()") def is_last_row(self): return self.get_subplotspec().rowspan.stop == self.get_gridspec().nrows + @cbook.deprecated("3.4", alternative="ax.get_subplotspec().is_first_col()") def is_first_col(self): return self.get_subplotspec().colspan.start == 0 + @cbook.deprecated("3.4", alternative="ax.get_subplotspec().is_last_col()") def is_last_col(self): return self.get_subplotspec().colspan.stop == self.get_gridspec().ncols @@ -109,8 +131,9 @@ def label_outer(self): 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() + ss = self.get_subplotspec() + lastrow = ss.is_last_row() + firstcol = ss.is_first_col() if not lastrow: for label in self.get_xticklabels(which="both"): label.set_visible(False) diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index f75612b74eb0..ba0b9a7040c4 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -1540,8 +1540,6 @@ def make_axes_gridspec(parent, *, location=None, orientation=None, aspect = 1 / aspect parent.set_subplotspec(ss_main) - parent.update_params() - parent._set_position(parent.figbox) parent.set_anchor(loc_settings["panchor"]) fig = parent.get_figure() diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index b9c523c8e72e..cf820369aecc 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -607,7 +607,7 @@ def autofmt_xdate( "3.3", message="Support for passing which=None to mean " "which='major' is deprecated since %(since)s and will be " "removed %(removal)s.") - allsubplots = all(hasattr(ax, 'is_last_row') for ax in self.axes) + allsubplots = all(hasattr(ax, 'get_subplotspec') for ax in self.axes) if len(self.axes) == 1: for label in self.axes[0].get_xticklabels(which=which): label.set_ha(ha) @@ -615,7 +615,7 @@ def autofmt_xdate( else: if allsubplots: for ax in self.get_axes(): - if ax.is_last_row(): + if ax.get_subplotspec().is_last_row(): for label in ax.get_xticklabels(which=which): label.set_ha(ha) label.set_rotation(rotation) @@ -2420,8 +2420,7 @@ def subplots_adjust(self, left=None, bottom=None, right=None, top=None, self.subplotpars.update(left, bottom, right, top, wspace, hspace) for ax in self.axes: if isinstance(ax, SubplotBase): - ax.update_params() - ax.set_position(ax.figbox) + ax._set_position(ax.get_subplotspec().get_position(self)) self.stale = True def ginput(self, n=1, timeout=30, show_clicks=True, diff --git a/lib/matplotlib/gridspec.py b/lib/matplotlib/gridspec.py index 0f6221581ed0..e3fcc625e5de 100644 --- a/lib/matplotlib/gridspec.py +++ b/lib/matplotlib/gridspec.py @@ -489,8 +489,8 @@ def update(self, **kwargs): if isinstance(ax, mpl.axes.SubplotBase): ss = ax.get_subplotspec().get_topmost_subplotspec() if ss.get_gridspec() == self: - ax.update_params() - ax._set_position(ax.figbox) + ax._set_position( + ax.get_subplotspec().get_position(ax.figure)) def get_subplot_params(self, figure=None): """ @@ -764,6 +764,19 @@ def colspan(self): c1, c2 = sorted([self.num1 % ncols, self.num2 % ncols]) return range(c1, c2 + 1) + def is_first_row(self): + return self.rowspan.start == 0 + + def is_last_row(self): + return self.rowspan.stop == self.get_gridspec().nrows + + def is_first_col(self): + return self.colspan.start == 0 + + def is_last_col(self): + return self.colspan.stop == self.get_gridspec().ncols + + @cbook._delete_parameter("3.4", "return_all") def get_position(self, figure, return_all=False): """ Update the subplot position from ``figure.subplotpars``. diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index 3159e7f2bba7..39f70ab847f3 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -365,7 +365,7 @@ def test_polycollection_close(): [[3., 0.], [3., 1.], [4., 1.], [4., 0.]]] fig = plt.figure() - ax = Axes3D(fig) + ax = fig.add_axes(Axes3D(fig)) colors = ['r', 'g', 'b', 'y', 'k'] zpos = list(range(5)) diff --git a/lib/matplotlib/tests/test_colorbar.py b/lib/matplotlib/tests/test_colorbar.py index 8307fceb3384..655f4940f37c 100644 --- a/lib/matplotlib/tests/test_colorbar.py +++ b/lib/matplotlib/tests/test_colorbar.py @@ -224,13 +224,13 @@ def test_remove_from_figure(use_gridspec): fig, ax = plt.subplots() sc = ax.scatter([1, 2], [3, 4], cmap="spring") sc.set_array(np.array([5, 6])) - pre_figbox = np.array(ax.figbox) + pre_position = ax.get_position() cb = fig.colorbar(sc, use_gridspec=use_gridspec) fig.subplots_adjust() cb.remove() fig.subplots_adjust() - post_figbox = np.array(ax.figbox) - assert (pre_figbox == post_figbox).all() + post_position = ax.get_position() + assert (pre_position.get_points() == post_position.get_points()).all() def test_colorbarbase(): diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index 295a6c80452c..f1abc04ff52e 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -170,7 +170,7 @@ def test_gca(): # Changing the projection will throw a warning assert fig.gca(polar=True) is not ax3 assert fig.gca(polar=True) is not ax2 - assert fig.gca().get_geometry() == (1, 1, 1) + assert fig.gca().get_subplotspec().get_geometry() == (1, 1, 0, 0) fig.sca(ax1) assert fig.gca(projection='rectilinear') is ax1 diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index d256e6210c41..61539fcb5a96 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -131,8 +131,6 @@ def __init__( pseudo_bbox = self.transLimits.inverted().transform([(0, 0), (1, 1)]) self._pseudo_w, self._pseudo_h = pseudo_bbox[1] - pseudo_bbox[0] - self.figure.add_axes(self) - # mplot3d currently manages its own spines and needs these turned off # for bounding box calculations for k in self.spines.keys(): diff --git a/lib/mpl_toolkits/tests/test_mplot3d.py b/lib/mpl_toolkits/tests/test_mplot3d.py index a2f5c0df8694..5ac5b1e62e4d 100644 --- a/lib/mpl_toolkits/tests/test_mplot3d.py +++ b/lib/mpl_toolkits/tests/test_mplot3d.py @@ -700,7 +700,7 @@ def test_add_collection3d_zs_scalar(): @mpl3d_image_comparison(['axes3d_labelpad.png'], remove_text=False) def test_axes3d_labelpad(): fig = plt.figure() - ax = Axes3D(fig) + ax = fig.add_axes(Axes3D(fig)) # labelpad respects rcParams assert ax.xaxis.labelpad == mpl.rcParams['axes.labelpad'] # labelpad can be set in set_label diff --git a/tutorials/intermediate/gridspec.py b/tutorials/intermediate/gridspec.py index 7fcf733d075b..bc19a618c8d9 100644 --- a/tutorials/intermediate/gridspec.py +++ b/tutorials/intermediate/gridspec.py @@ -246,10 +246,11 @@ def squiggle_xy(a, b, c, d, i=np.arange(0.0, 2*np.pi, 0.05)): # show only the outside spines for ax in fig11.get_axes(): - ax.spines['top'].set_visible(ax.is_first_row()) - ax.spines['bottom'].set_visible(ax.is_last_row()) - ax.spines['left'].set_visible(ax.is_first_col()) - ax.spines['right'].set_visible(ax.is_last_col()) + ss = ax.get_subplotspec() + ax.spines['top'].set_visible(ss.is_first_row()) + ax.spines['bottom'].set_visible(ss.is_last_row()) + ax.spines['left'].set_visible(ss.is_first_col()) + ax.spines['right'].set_visible(ss.is_last_col()) plt.show()