Skip to content

Commit 4167f13

Browse files
ebardieeBardieCt
authored andcommitted
Fix missing tick labels on twinned axes.
Background: Multi-row and/or multi-column subplots can utilize shared axes. An external share happens at axis creation when a sharex or sharey parameter is specified. An internal share, or twinning, occurs when an overlayed axis is created by the Axes.twinx() or Axes.twiny() calls. The two types of sharing can be distinguished after the fact in the following manner. If two axes sharing an axis also have the same position, they are not in an external axis share, they are twinned. For externally shared axes Pandas automatically removes tick labels for all but the last row and/or first column in ./pandas/plotting/_matplotlib/tools.py's function _handle_shared_axes(). The problem: _handle_shared_axes() should be interested in externally shared axes, whether or not they are also twinned. It should, but doesn't, ignore axes which are only twinned. Which means that twinned-only axes wrongly lose their tick labels. The cure: This commit introduces _has_externally_shared_axis() which identifies externally shared axes and uses it to expand upon the existing use of len(Axes.get_shared_{x,y}_axes().get_siblings(a{x,y})) in _handle_shared_axes() which miss these cases.
1 parent 930a7f8 commit 4167f13

File tree

2 files changed

+99
-3
lines changed

2 files changed

+99
-3
lines changed

pandas/plotting/_matplotlib/tools.py

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,52 @@ def _remove_labels_from_axis(axis):
288288
axis.get_label().set_visible(False)
289289

290290

291+
def _has_externally_shared_axis(ax1, compare_axis):
292+
"""
293+
Return whether an axis is externally shared.
294+
295+
Parameters
296+
----------
297+
ax1 : matplotlib.axes
298+
Axis to query.
299+
compare_axis : str
300+
`"x"` or `"y"` according to whether the X-axis or Y-axis is being
301+
compared.
302+
303+
Notes
304+
-----
305+
If two axes with different positions are sharing an axis, they can be
306+
referred to as *externally* sharing the common axis.
307+
308+
If two axes sharing an axis also have the same position, they can be
309+
referred to as *internally* sharing the common axis (a.k.a twinning).
310+
311+
_handle_shared_axes() is only interested in axes externally sharing an
312+
axis, regardless of whether either of the axes is also internally sharing
313+
with a third axis.
314+
"""
315+
if compare_axis == "x":
316+
axes = ax1.get_shared_x_axes()
317+
elif compare_axis == "y":
318+
axes = ax1.get_shared_y_axes()
319+
else:
320+
raise ValueError(
321+
"_has_externally_shared_axis() needs 'x' or 'y' as a second parameter"
322+
)
323+
324+
axes = axes.get_siblings(ax1)
325+
326+
# Retain ax1 and any of its siblings which aren't in the same position as it
327+
ax1_points = ax1.get_position().get_points()
328+
axes = [
329+
ax2
330+
for ax2 in axes
331+
if ax2 is ax1 or not np.array_equal(ax1_points, ax2.get_position().get_points())
332+
]
333+
334+
return len(axes) > 1
335+
336+
291337
def _handle_shared_axes(axarr, nplots, naxes, nrows, ncols, sharex, sharey):
292338
if nplots > 1:
293339
if compat._mpl_ge_3_2_0():
@@ -311,7 +357,7 @@ def _handle_shared_axes(axarr, nplots, naxes, nrows, ncols, sharex, sharey):
311357
# the last in the column, because below is no subplot/gap.
312358
if not layout[row_num(ax) + 1, col_num(ax)]:
313359
continue
314-
if sharex or len(ax.get_shared_x_axes().get_siblings(ax)) > 1:
360+
if sharex or _has_externally_shared_axis(ax, "x"):
315361
_remove_labels_from_axis(ax.xaxis)
316362

317363
except IndexError:
@@ -320,7 +366,7 @@ def _handle_shared_axes(axarr, nplots, naxes, nrows, ncols, sharex, sharey):
320366
for ax in axarr:
321367
if ax.is_last_row():
322368
continue
323-
if sharex or len(ax.get_shared_x_axes().get_siblings(ax)) > 1:
369+
if sharex or _has_externally_shared_axis(ax, "x"):
324370
_remove_labels_from_axis(ax.xaxis)
325371

326372
if ncols > 1:
@@ -330,7 +376,7 @@ def _handle_shared_axes(axarr, nplots, naxes, nrows, ncols, sharex, sharey):
330376
# have a subplot there, we can skip the layout test
331377
if ax.is_first_col():
332378
continue
333-
if sharey or len(ax.get_shared_y_axes().get_siblings(ax)) > 1:
379+
if sharey or _has_externally_shared_axis(ax, "y"):
334380
_remove_labels_from_axis(ax.yaxis)
335381

336382

pandas/tests/plotting/test_misc.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,3 +425,53 @@ def test_dictionary_color(self):
425425
ax = df1.plot(kind="line", color=dic_color)
426426
colors = [rect.get_color() for rect in ax.get_lines()[0:2]]
427427
assert all(color == expected[index] for index, color in enumerate(colors))
428+
429+
def test_has_externally_shared_axis(self):
430+
func = plotting._matplotlib.tools._has_externally_shared_axis
431+
432+
# Test x-axis
433+
fig = self.plt.figure()
434+
plots = fig.subplots(2, 4)
435+
436+
# Create *externally* shared axes for first and third columns
437+
plots[0][0] = self.plt.subplot(231, sharex=plots[1][0])
438+
plots[0][2] = self.plt.subplot(233, sharex=plots[1][2])
439+
440+
# Create *internally* shared axes for second and third columns
441+
plots[0][1].twinx()
442+
plots[0][2].twinx()
443+
444+
# First column is only externally shared
445+
# Second column is only internally shared
446+
# Third column is both
447+
# Fourth column is neither
448+
assert func(plots[0][0], "x")
449+
assert not func(plots[0][1], "x")
450+
assert func(plots[0][2], "x")
451+
assert not func(plots[0][3], "x")
452+
453+
# Test y-axis
454+
fig = self.plt.figure()
455+
plots = fig.subplots(4, 2)
456+
457+
# Create *externally* shared axes for first and third rows
458+
plots[0][0] = self.plt.subplot(321, sharey=plots[0][1])
459+
plots[2][0] = self.plt.subplot(325, sharey=plots[2][1])
460+
461+
# Create *internally* shared axes for second and third rows
462+
plots[1][0].twiny()
463+
plots[2][0].twiny()
464+
465+
# First row is only externally shared
466+
# Second row is only internally shared
467+
# Third row is both
468+
# Fourth row is neither
469+
assert func(plots[0][0], "y")
470+
assert not func(plots[1][0], "y")
471+
assert func(plots[2][0], "y")
472+
assert not func(plots[3][0], "y")
473+
474+
# Check that an invalid compare_axis value triggers the expected exception
475+
msg = "needs 'x' or 'y' as a second parameter"
476+
with pytest.raises(ValueError, match=msg):
477+
func(plots[0][0], "z")

0 commit comments

Comments
 (0)