Skip to content

Commit 235e5b7

Browse files
authored
Fix missing tick labels on twinned axes. (#33767)
1 parent 946326a commit 235e5b7

File tree

3 files changed

+168
-3
lines changed

3 files changed

+168
-3
lines changed

doc/source/whatsnew/v1.2.0.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ Plotting
334334

335335
- Bug in :meth:`DataFrame.plot` was rotating xticklabels when ``subplots=True``, even if the x-axis wasn't an irregular time series (:issue:`29460`)
336336
- Bug in :meth:`DataFrame.plot` where a marker letter in the ``style`` keyword sometimes causes a ``ValueError`` (:issue:`21003`)
337+
- Twinned axes were losing their tick labels which should only happen to all but the last row or column of 'externally' shared axes (:issue:`33819`)
337338

338339
Groupby/resample/rolling
339340
^^^^^^^^^^^^^^^^^^^^^^^^

pandas/plotting/_matplotlib/tools.py

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,56 @@ def _remove_labels_from_axis(axis: "Axis"):
297297
axis.get_label().set_visible(False)
298298

299299

300+
def _has_externally_shared_axis(ax1: "matplotlib.axes", compare_axis: "str") -> bool:
301+
"""
302+
Return whether an axis is externally shared.
303+
304+
Parameters
305+
----------
306+
ax1 : matplotlib.axes
307+
Axis to query.
308+
compare_axis : str
309+
`"x"` or `"y"` according to whether the X-axis or Y-axis is being
310+
compared.
311+
312+
Returns
313+
-------
314+
bool
315+
`True` if the axis is externally shared. Otherwise `False`.
316+
317+
Notes
318+
-----
319+
If two axes with different positions are sharing an axis, they can be
320+
referred to as *externally* sharing the common axis.
321+
322+
If two axes sharing an axis also have the same position, they can be
323+
referred to as *internally* sharing the common axis (a.k.a twinning).
324+
325+
_handle_shared_axes() is only interested in axes externally sharing an
326+
axis, regardless of whether either of the axes is also internally sharing
327+
with a third axis.
328+
"""
329+
if compare_axis == "x":
330+
axes = ax1.get_shared_x_axes()
331+
elif compare_axis == "y":
332+
axes = ax1.get_shared_y_axes()
333+
else:
334+
raise ValueError(
335+
"_has_externally_shared_axis() needs 'x' or 'y' as a second parameter"
336+
)
337+
338+
axes = axes.get_siblings(ax1)
339+
340+
# Retain ax1 and any of its siblings which aren't in the same position as it
341+
ax1_points = ax1.get_position().get_points()
342+
343+
for ax2 in axes:
344+
if not np.array_equal(ax1_points, ax2.get_position().get_points()):
345+
return True
346+
347+
return False
348+
349+
300350
def handle_shared_axes(
301351
axarr: Iterable["Axes"],
302352
nplots: int,
@@ -328,7 +378,7 @@ def handle_shared_axes(
328378
# the last in the column, because below is no subplot/gap.
329379
if not layout[row_num(ax) + 1, col_num(ax)]:
330380
continue
331-
if sharex or len(ax.get_shared_x_axes().get_siblings(ax)) > 1:
381+
if sharex or _has_externally_shared_axis(ax, "x"):
332382
_remove_labels_from_axis(ax.xaxis)
333383

334384
except IndexError:
@@ -337,7 +387,7 @@ def handle_shared_axes(
337387
for ax in axarr:
338388
if ax.is_last_row():
339389
continue
340-
if sharex or len(ax.get_shared_x_axes().get_siblings(ax)) > 1:
390+
if sharex or _has_externally_shared_axis(ax, "x"):
341391
_remove_labels_from_axis(ax.xaxis)
342392

343393
if ncols > 1:
@@ -347,7 +397,7 @@ def handle_shared_axes(
347397
# have a subplot there, we can skip the layout test
348398
if ax.is_first_col():
349399
continue
350-
if sharey or len(ax.get_shared_y_axes().get_siblings(ax)) > 1:
400+
if sharey or _has_externally_shared_axis(ax, "y"):
351401
_remove_labels_from_axis(ax.yaxis)
352402

353403

pandas/tests/plotting/test_misc.py

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

0 commit comments

Comments
 (0)