Skip to content

Commit f1e6f03

Browse files
committed
Fix missing tick labels on twinned axes.
1 parent a0d6d06 commit f1e6f03

File tree

3 files changed

+150
-3
lines changed

3 files changed

+150
-3
lines changed

doc/source/whatsnew/v1.2.0.rst

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

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

335336
Groupby/resample/rolling
336337
^^^^^^^^^^^^^^^^^^^^^^^^

pandas/plotting/_matplotlib/tools.py

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,57 @@ 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+
axes = [
343+
ax2
344+
for ax2 in axes
345+
if ax2 is ax1 or not np.array_equal(ax1_points, ax2.get_position().get_points())
346+
]
347+
348+
return len(axes) > 1
349+
350+
300351
def handle_shared_axes(
301352
axarr: Iterable["Axes"],
302353
nplots: int,
@@ -328,7 +379,7 @@ def handle_shared_axes(
328379
# the last in the column, because below is no subplot/gap.
329380
if not layout[row_num(ax) + 1, col_num(ax)]:
330381
continue
331-
if sharex or len(ax.get_shared_x_axes().get_siblings(ax)) > 1:
382+
if sharex or _has_externally_shared_axis(ax, "x"):
332383
_remove_labels_from_axis(ax.xaxis)
333384

334385
except IndexError:
@@ -337,7 +388,7 @@ def handle_shared_axes(
337388
for ax in axarr:
338389
if ax.is_last_row():
339390
continue
340-
if sharex or len(ax.get_shared_x_axes().get_siblings(ax)) > 1:
391+
if sharex or _has_externally_shared_axis(ax, "x"):
341392
_remove_labels_from_axis(ax.xaxis)
342393

343394
if ncols > 1:
@@ -347,7 +398,7 @@ def handle_shared_axes(
347398
# have a subplot there, we can skip the layout test
348399
if ax.is_first_col():
349400
continue
350-
if sharey or len(ax.get_shared_y_axes().get_siblings(ax)) > 1:
401+
if sharey or _has_externally_shared_axis(ax, "y"):
351402
_remove_labels_from_axis(ax.yaxis)
352403

353404

pandas/tests/plotting/test_misc.py

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

0 commit comments

Comments
 (0)