Skip to content

Commit 1822447

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 5124efd commit 1822447

File tree

3 files changed

+150
-3
lines changed

3 files changed

+150
-3
lines changed

doc/source/whatsnew/v1.1.0.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1108,6 +1108,7 @@ Plotting
11081108
- Bug in :meth:`DataFrame.plot.scatter` was adding a colorbar to the plot even if the argument `c` was assigned to a column containing color names (:issue:`34316`)
11091109
- Bug in :meth:`pandas.plotting.bootstrap_plot` was causing cluttered axes and overlapping labels (:issue:`34905`)
11101110
- Bug in :meth:`DataFrame.plot.scatter` caused an error when plotting variable marker sizes (:issue:`32904`)
1111+
- 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`)
11111112

11121113
Groupby/resample/rolling
11131114
^^^^^^^^^^^^^^^^^^^^^^^^

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

0 commit comments

Comments
 (0)