Skip to content

Commit c96e671

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 506eb54 commit c96e671

File tree

2 files changed

+146
-3
lines changed

2 files changed

+146
-3
lines changed

pandas/plotting/_matplotlib/tools.py

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

290290

291+
def _has_externally_shared_axis(ax1:"matplotlib.axes", compare_axis:"str") -> bool:
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+
Returns
304+
-------
305+
bool
306+
`True` if the axis is externally shared. Otherwise `False`.
307+
308+
Notes
309+
-----
310+
If two axes with different positions are sharing an axis, they can be
311+
referred to as *externally* sharing the common axis.
312+
313+
If two axes sharing an axis also have the same position, they can be
314+
referred to as *internally* sharing the common axis (a.k.a twinning).
315+
316+
_handle_shared_axes() is only interested in axes externally sharing an
317+
axis, regardless of whether either of the axes is also internally sharing
318+
with a third axis.
319+
"""
320+
if compare_axis == "x":
321+
axes = ax1.get_shared_x_axes()
322+
elif compare_axis == "y":
323+
axes = ax1.get_shared_y_axes()
324+
else:
325+
raise ValueError(
326+
"_has_externally_shared_axis() needs 'x' or 'y' as a second parameter"
327+
)
328+
329+
axes = axes.get_siblings(ax1)
330+
331+
# Retain ax1 and any of its siblings which aren't in the same position as it
332+
ax1_points = ax1.get_position().get_points()
333+
axes = [
334+
ax2
335+
for ax2 in axes
336+
if ax2 is ax1 or not np.array_equal(ax1_points, ax2.get_position().get_points())
337+
]
338+
339+
return len(axes) > 1
340+
341+
291342
def _handle_shared_axes(axarr, nplots, naxes, nrows, ncols, sharex, sharey):
292343
if nplots > 1:
293344
if compat._mpl_ge_3_2_0():
@@ -311,7 +362,7 @@ def _handle_shared_axes(axarr, nplots, naxes, nrows, ncols, sharex, sharey):
311362
# the last in the column, because below is no subplot/gap.
312363
if not layout[row_num(ax) + 1, col_num(ax)]:
313364
continue
314-
if sharex or len(ax.get_shared_x_axes().get_siblings(ax)) > 1:
365+
if sharex or _has_externally_shared_axis(ax, "x"):
315366
_remove_labels_from_axis(ax.xaxis)
316367

317368
except IndexError:
@@ -320,7 +371,7 @@ def _handle_shared_axes(axarr, nplots, naxes, nrows, ncols, sharex, sharey):
320371
for ax in axarr:
321372
if ax.is_last_row():
322373
continue
323-
if sharex or len(ax.get_shared_x_axes().get_siblings(ax)) > 1:
374+
if sharex or _has_externally_shared_axis(ax, "x"):
324375
_remove_labels_from_axis(ax.xaxis)
325376

326377
if ncols > 1:
@@ -330,7 +381,7 @@ def _handle_shared_axes(axarr, nplots, naxes, nrows, ncols, sharex, sharey):
330381
# have a subplot there, we can skip the layout test
331382
if ax.is_first_col():
332383
continue
333-
if sharey or len(ax.get_shared_y_axes().get_siblings(ax)) > 1:
384+
if sharey or _has_externally_shared_axis(ax, "y"):
334385
_remove_labels_from_axis(ax.yaxis)
335386

336387

pandas/tests/plotting/test_misc.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,3 +429,95 @@ def test_dictionary_color(self):
429429
ax = df1.plot(kind="line", color=dic_color)
430430
colors = [rect.get_color() for rect in ax.get_lines()[0:2]]
431431
assert all(color == expected[index] for index, color in enumerate(colors))
432+
433+
def test_has_externally_shared_axis(self):
434+
func = plotting._matplotlib.tools._has_externally_shared_axis
435+
436+
# Test x-axis
437+
fig = self.plt.figure()
438+
plots = fig.subplots(2, 4)
439+
440+
# Create *externally* shared axes for first and third columns
441+
plots[0][0] = self.plt.subplot(231, sharex=plots[1][0])
442+
plots[0][2] = self.plt.subplot(233, sharex=plots[1][2])
443+
444+
# Create *internally* shared axes for second and third columns
445+
plots[0][1].twinx()
446+
plots[0][2].twinx()
447+
448+
# First column is only externally shared
449+
# Second column is only internally shared
450+
# Third column is both
451+
# Fourth column is neither
452+
assert func(plots[0][0], "x")
453+
assert not func(plots[0][1], "x")
454+
assert func(plots[0][2], "x")
455+
assert not func(plots[0][3], "x")
456+
457+
# Test y-axis
458+
fig = self.plt.figure()
459+
plots = fig.subplots(4, 2)
460+
461+
# Create *externally* shared axes for first and third rows
462+
plots[0][0] = self.plt.subplot(321, sharey=plots[0][1])
463+
plots[2][0] = self.plt.subplot(325, sharey=plots[2][1])
464+
465+
# Create *internally* shared axes for second and third rows
466+
plots[1][0].twiny()
467+
plots[2][0].twiny()
468+
469+
# First row is only externally shared
470+
# Second row is only internally shared
471+
# Third row is both
472+
# Fourth row is neither
473+
assert func(plots[0][0], "y")
474+
assert not func(plots[1][0], "y")
475+
assert func(plots[2][0], "y")
476+
assert not func(plots[3][0], "y")
477+
478+
# Check that an invalid compare_axis value triggers the expected exception
479+
msg = "needs 'x' or 'y' as a second parameter"
480+
with pytest.raises(ValueError, match=msg):
481+
func(plots[0][0], "z")
482+
483+
def test_externally_shared_axes(self):
484+
# Create data
485+
df = DataFrame({"a": np.random.randn(1000), "b": np.random.randn(1000)})
486+
487+
# Create figure
488+
fig = self.plt.figure()
489+
plots = fig.subplots(2, 3)
490+
491+
# Create *externally* shared axes
492+
plots[0][0] = self.plt.subplot(231, sharex=plots[1][0])
493+
# note: no plots[0][1] that's the twin only case
494+
plots[0][2] = self.plt.subplot(233, sharex=plots[1][2])
495+
496+
# Create *internally* shared axes
497+
# note: no plots[0][0] that's the external only case
498+
twin_ax1 = plots[0][1].twinx()
499+
twin_ax2 = plots[0][2].twinx()
500+
501+
# Plot data to primary axes
502+
df["a"].plot(ax=plots[0][0], title="External share only").set_xlabel(
503+
"this label should never be visible"
504+
)
505+
df["a"].plot(ax=plots[1][0])
506+
507+
df["a"].plot(ax=plots[0][1], title="Internal share (twin) only").set_xlabel(
508+
"this label should always be visible"
509+
)
510+
df["a"].plot(ax=plots[1][1])
511+
512+
df["a"].plot(ax=plots[0][2], title="Both").set_xlabel(
513+
"this label should never be visible"
514+
)
515+
df["a"].plot(ax=plots[1][2])
516+
517+
# Plot data to twinned axes
518+
df["b"].plot(ax=twin_ax1, color="green")
519+
df["b"].plot(ax=twin_ax2, color="yellow")
520+
521+
assert not plots[0][0].xaxis.get_label().get_visible()
522+
assert plots[0][1].xaxis.get_label().get_visible()
523+
assert not plots[0][2].xaxis.get_label().get_visible()

0 commit comments

Comments
 (0)