Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 7b376d5

Browse files
committed
Fix y-label overlap with tick labels for non-default rotations (#19029)
`YAxis._update_label_position` previously assumed a 90° label rotation when computing the label's position, so y-labels with other rotations (e.g. ``rotation=270`` for right-side colorbar labels) could overlap the tick labels. The label position is now computed from the rendered label's actual extent, so it sits clear of the tick labels for any rotation. This implements @jklymak's suggestion in the issue thread of checking the rendered label extent against the tick labels in `_update_label_position`. The fix is a no-op for the default ``rotation=90`` case, so existing image baselines are unaffected. Tests added in test_axes.py cover the generic axis-layer fix (parametrized over 7 rotations × 2 label_positions) plus an ``align_ylabels`` regression that guards against the per-axis overhang correction breaking sibling alignment when rotations differ. A colorbar smoke test in test_colorbar.py exercises the original user-facing symptom from the issue via ``cbar.set_label(..., rotation=270)``.
1 parent 462e6a6 commit 7b376d5

4 files changed

Lines changed: 94 additions & 4 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
y-axis labels with non-default rotation no longer overlap tick labels
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
4+
Setting a y-axis label with a rotation other than the default (e.g.
5+
``ax.set_ylabel("...", rotation=270)``, or ``cbar.set_label("...",
6+
rotation=270)`` on a colorbar) previously could cause the label to
7+
overlap the tick labels because the automatic label placement assumed a
8+
90° rotation. The label position is now computed from the rendered
9+
label's actual extent, so it sits clear of the tick labels for any
10+
rotation. This may shift the position of y-labels that use non-default
11+
rotations.

lib/matplotlib/axis.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2735,14 +2735,27 @@ def _update_label_position(self, renderer):
27352735
# Union with extents of the left spine if present, of the axes otherwise.
27362736
bbox = mtransforms.Bbox.union([
27372737
*bboxes, self.axes.spines.get("left", self.axes).get_window_extent()])
2738-
self.label.set_position(
2739-
(bbox.x0 - self.labelpad * self.get_figure(root=True).dpi / 72, y))
2738+
target_x = bbox.x0 - self.labelpad * self.get_figure(root=True).dpi / 72
2739+
self.label.set_position((target_x, y))
2740+
# The vertical alignment set in `set_label_position` assumes a
2741+
# 90° rotation; for any other rotation the rotated label may
2742+
# extend back toward the tick labels and overlap them. Shift
2743+
# the label outward by the overhang. See issue #19029.
2744+
label_bbox = self.label.get_window_extent(renderer)
2745+
overhang = label_bbox.x1 - target_x
2746+
if overhang > 0:
2747+
self.label.set_position((target_x - overhang, y))
27402748
else:
27412749
# Union with extents of the right spine if present, of the axes otherwise.
27422750
bbox = mtransforms.Bbox.union([
27432751
*bboxes2, self.axes.spines.get("right", self.axes).get_window_extent()])
2744-
self.label.set_position(
2745-
(bbox.x1 + self.labelpad * self.get_figure(root=True).dpi / 72, y))
2752+
target_x = bbox.x1 + self.labelpad * self.get_figure(root=True).dpi / 72
2753+
self.label.set_position((target_x, y))
2754+
# See comment in the 'left' branch above (issue #19029).
2755+
label_bbox = self.label.get_window_extent(renderer)
2756+
overhang = target_x - label_bbox.x0
2757+
if overhang > 0:
2758+
self.label.set_position((target_x + overhang, y))
27462759

27472760
def _update_offset_text_position(self, bboxes, bboxes2):
27482761
"""

lib/matplotlib/tests/test_axes.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9070,6 +9070,54 @@ def test_ylabel_ha_with_position(ha):
90709070
assert ax.yaxis.label.get_ha() == ha
90719071

90729072

9073+
@pytest.mark.parametrize('label_position', ['left', 'right'])
9074+
@pytest.mark.parametrize('rotation', [0, 45, 90, 135, 180, 270, -90])
9075+
def test_ylabel_no_overlap_with_ticklabels(label_position, rotation):
9076+
"""Regression test for #19029: a y-label with rotation other than 90°
9077+
must not overlap the tick labels regardless of label_position."""
9078+
fig, ax = plt.subplots()
9079+
ax.plot([1, 3, 2])
9080+
ax.yaxis.set_label_position(label_position)
9081+
if label_position == 'right':
9082+
ax.yaxis.tick_right()
9083+
ax.set_ylabel("test label", rotation=rotation)
9084+
fig.canvas.draw()
9085+
renderer = fig.canvas.get_renderer()
9086+
label_bbox = ax.yaxis.label.get_window_extent(renderer)
9087+
tick_bboxes = [t.get_window_extent(renderer)
9088+
for t in ax.yaxis.get_ticklabels() if t.get_visible()]
9089+
tick_union = mtransforms.Bbox.union(tick_bboxes)
9090+
if label_position == 'right':
9091+
assert label_bbox.x0 >= tick_union.x1
9092+
else:
9093+
assert label_bbox.x1 <= tick_union.x0
9094+
9095+
9096+
@pytest.mark.parametrize('label_position', ['left', 'right'])
9097+
def test_align_ylabels_mixed_rotation(label_position):
9098+
"""Issue #19029: ``fig.align_ylabels`` must keep two y-labels visually
9099+
aligned even when they use different rotations. The per-axis overhang
9100+
correction in `_update_label_position` shifts each axis independently;
9101+
this test guards against that breaking sibling alignment."""
9102+
fig, axs = plt.subplots(2, figsize=(4, 6))
9103+
for ax in axs:
9104+
ax.plot([1, 30, 2])
9105+
ax.yaxis.set_label_position(label_position)
9106+
if label_position == 'right':
9107+
ax.yaxis.tick_right()
9108+
axs[0].set_ylabel('first', rotation=90)
9109+
axs[1].set_ylabel('second', rotation=270)
9110+
fig.align_ylabels(axs)
9111+
fig.canvas.draw()
9112+
renderer = fig.canvas.get_renderer()
9113+
bbox0 = axs[0].yaxis.label.get_window_extent(renderer)
9114+
bbox1 = axs[1].yaxis.label.get_window_extent(renderer)
9115+
# Both visible label bboxes should occupy the same horizontal range,
9116+
# even though the underlying anchor positions differ between rotations.
9117+
assert bbox0.x0 == pytest.approx(bbox1.x0, abs=0.5)
9118+
assert bbox0.x1 == pytest.approx(bbox1.x1, abs=0.5)
9119+
9120+
90739121
def test_bar_label_location_vertical():
90749122
ax = plt.gca()
90759123
xs, heights = [1, 2], [3, -4]

lib/matplotlib/tests/test_colorbar.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,24 @@ def test_colorbar_label():
733733
assert cbar3.ax.get_xlabel() == 'horizontal cbar'
734734

735735

736+
def test_colorbar_label_rotation_no_overlap():
737+
"""Smoke test for #19029: a rotation=270 colorbar label must not
738+
overlap negative tick labels (the original user-facing symptom)."""
739+
fig, ax = plt.subplots()
740+
im = ax.imshow([[-100, 0], [50, 100]])
741+
cbar = fig.colorbar(im, ax=ax)
742+
cbar.set_label('RT(1ax) - RT(2ax) [ms]', rotation=270)
743+
fig.canvas.draw()
744+
renderer = fig.canvas.get_renderer()
745+
label_bbox = cbar.ax.yaxis.label.get_window_extent(renderer)
746+
# Colorbar labels live on the right, so the label must sit clear of
747+
# the rightmost visible tick-label edge.
748+
tick_x1_max = max(t.get_window_extent(renderer).x1
749+
for t in cbar.ax.yaxis.get_ticklabels()
750+
if t.get_visible())
751+
assert label_bbox.x0 >= tick_x1_max
752+
753+
736754
# TODO: tighten tolerance after baseline image is regenerated for text overhaul
737755
@image_comparison(['colorbar_keeping_xlabel.png'], style='mpl20', tol=0.03)
738756
def test_keeping_xlabel():

0 commit comments

Comments
 (0)