Skip to content

Improve handling of subplots spanning multiple gridspec cells. #13544

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

Merged
merged 1 commit into from
Sep 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions doc/api/next_api_changes/2019-02-28-AL.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
``Axes.label_outer``, ``Axes.is_last_row``, and ``Axes.is_last_col`` now work correctly for axes spanning multiple rows or columns
``````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````

``Axes.label_outer`` now correctly keep the x labels and tick labels visible
for Axes spanning multiple rows, as long as they cover the last row of the Axes
grid. (This is consistent with keeping the y labels and tick labels visible
for Axes spanning multiple columns as long as they cover the first column of
the Axes grid.)

The ``Axes.is_last_row`` and ``Axes.is_last_col`` methods now correctly return
True for Axes spanning multiple rows, as long as they cover the last row or
column respectively. Again this is consistent with the behavior for axes
covering the first row or column.

The ``Axes.rowNum`` and ``Axes.colNum`` attributes are deprecated, as they only
refer to the first grid cell covered by the Axes. Instead, use the new
``ax.get_subplotspec().rowspan`` and ``ax.get_subplotspec().colspan``
properties, which are `range` objects indicating the whole span of rows and
columns covered by the subplot.

(Note that all methods and attributes mentioned here actually only exist on
the ``Subplot`` subclass of `Axes`, which is used for grid-positioned Axes but
not for Axes positioned directly in absolute coordinates.)

The `.GridSpec` class gained the ``nrows`` and ``ncols`` properties as more
explicit synonyms for the parameters returned by ``GridSpec.get_geometry``.
27 changes: 17 additions & 10 deletions lib/matplotlib/axes/_subplots.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import functools
import uuid

from matplotlib import docstring
from matplotlib import cbook, docstring
import matplotlib.artist as martist
from matplotlib.axes._axes import Axes
from matplotlib.gridspec import GridSpec, SubplotSpec
Expand Down Expand Up @@ -125,26 +125,33 @@ def get_gridspec(self):

def update_params(self):
"""update the subplot position from fig.subplotpars"""

self.figbox, self.rowNum, self.colNum, self.numRows, self.numCols = \
self.figbox, _, _, self.numRows, self.numCols = \
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why keep self.numRows\Cols around?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because I don't have a particularly good reason to deprecate them, whereas I'd say rowNum and colNum are actively encouraging people (... the few who'd touch that part of the codebase) to ignore the case of subplots spanning multiple cells.

self.get_subplotspec().get_position(self.figure,
return_all=True)

def is_first_col(self):
return self.colNum == 0
@cbook.deprecated("3.2", alternative="ax.get_subplotspec().rowspan.start")
def rowNum(self):
return self.get_subplotspec().rowspan.start

@cbook.deprecated("3.2", alternative="ax.get_subplotspec().colspan.start")
def colNum(self):
return self.get_subplotspec().colspan.start

def is_first_row(self):
return self.rowNum == 0
return self.get_subplotspec().rowspan.start == 0

def is_last_row(self):
return self.rowNum == self.numRows - 1
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.colNum == self.numCols - 1
return self.get_subplotspec().colspan.stop == self.get_gridspec().ncols

# COVERAGE NOTE: Never used internally.
def label_outer(self):
"""Only show "outer" labels and tick labels.
"""
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.
Expand Down
17 changes: 17 additions & 0 deletions lib/matplotlib/gridspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ def __repr__(self):
optionals=height_arg + width_arg,
)

nrows = property(lambda self: self._nrows,
doc="The number of rows in the grid.")
ncols = property(lambda self: self._ncols,
doc="The number of columns in the grid.")

def get_geometry(self):
"""
Return a tuple containing the number of rows and columns in the grid.
Expand Down Expand Up @@ -566,6 +571,18 @@ def get_rows_columns(self):
row_stop, col_stop = divmod(self.num2, ncols)
return nrows, ncols, row_start, row_stop, col_start, col_stop

@property
def rowspan(self):
"""The rows spanned by this subplot, as a `range` object."""
ncols = self.get_gridspec().ncols
return range(self.num1 // ncols, self.num2 // ncols + 1)

@property
def colspan(self):
"""The columns spanned by this subplot, as a `range` object."""
ncols = self.get_gridspec().ncols
return range(self.num1 % ncols, self.num2 % ncols + 1)

def get_position(self, figure, return_all=False):
"""
Update the subplot position from ``figure.subplotpars``.
Expand Down
29 changes: 23 additions & 6 deletions lib/matplotlib/tests/test_subplots.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,13 @@ def check_shared(axs, x_shared, y_shared):


def check_visible(axs, x_visible, y_visible):
def tostr(v):
return "invisible" if v else "visible"

for ax, vx, vy in zip(axs, x_visible, y_visible):
for i, (ax, vx, vy) in enumerate(zip(axs, x_visible, y_visible)):
for l in ax.get_xticklabels() + [ax.get_xaxis().offsetText]:
assert l.get_visible() == vx, \
"X axis was incorrectly %s" % (tostr(vx))
f"Visibility of x axis #{i} is incorrectly {vx}"
for l in ax.get_yticklabels() + [ax.get_yaxis().offsetText]:
assert l.get_visible() == vy, \
"Y axis was incorrectly %s" % (tostr(vy))
f"Visibility of y axis #{i} is incorrectly {vy}"


def test_shared():
Expand Down Expand Up @@ -99,6 +96,26 @@ def test_shared():
check_visible(axs, [False, False, True, True], [True, False, True, False])


def test_label_outer_span():
fig = plt.figure()
gs = fig.add_gridspec(3, 3)
# +---+---+---+
# | 1 | |
# +---+---+---+
# | | | 3 |
# + 2 +---+---+
# | | 4 | |
# +---+---+---+
a1 = fig.add_subplot(gs[0, 0:2])
a2 = fig.add_subplot(gs[1:3, 0])
a3 = fig.add_subplot(gs[1, 2])
a4 = fig.add_subplot(gs[2, 1])
for ax in fig.axes:
ax.label_outer()
check_visible(
fig.axes, [False, True, False, True], [True, True, False, False])


def test_shared_and_moved():
# test if sharey is on, but then tick_left is called that labels don't
# re-appear. Seaborn does this just to be sure yaxis is on left...
Expand Down