Skip to content

Commit c074a15

Browse files
committed
Implement xtick and ytick rotation_modes
1 parent 0439b37 commit c074a15

File tree

6 files changed

+119
-16
lines changed

6 files changed

+119
-16
lines changed

lib/matplotlib/axis.py

+26-9
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,11 @@ def _apply_params(self, **kwargs):
346346
if k in _gridline_param_names}
347347
self.gridline.set(**grid_kw)
348348

349+
if 'rotation_mode' in kwargs:
350+
rotation_mode = kwargs.pop('rotation_mode')
351+
self.label1.set_rotation_mode(rotation_mode)
352+
self.label2.set_rotation_mode(rotation_mode)
353+
349354
def update_position(self, loc):
350355
"""Set the location of tick in data coords with scalar *loc*."""
351356
raise NotImplementedError('Derived must override')
@@ -1041,14 +1046,15 @@ def get_tick_params(self, which='major'):
10411046
10421047
"""
10431048
_api.check_in_list(['major', 'minor'], which=which)
1049+
is_x_axis = True if isinstance(self, XAxis) else False
10441050
if which == 'major':
10451051
return self._translate_tick_params(
1046-
self._major_tick_kw, reverse=True
1047-
)
1048-
return self._translate_tick_params(self._minor_tick_kw, reverse=True)
1052+
self._major_tick_kw, reverse=True, is_x_axis=is_x_axis)
1053+
return self._translate_tick_params(
1054+
self._minor_tick_kw, reverse=True, is_x_axis=is_x_axis)
10491055

10501056
@staticmethod
1051-
def _translate_tick_params(kw, reverse=False):
1057+
def _translate_tick_params(kw, reverse=False, is_x_axis=None):
10521058
"""
10531059
Translate the kwargs supported by `.Axis.set_tick_params` to kwargs
10541060
supported by `.Tick._apply_params`.
@@ -1072,7 +1078,7 @@ def _translate_tick_params(kw, reverse=False):
10721078
'tick1On', 'tick2On', 'label1On', 'label2On',
10731079
'length', 'direction', 'left', 'bottom', 'right', 'top',
10741080
'labelleft', 'labelbottom', 'labelright', 'labeltop',
1075-
'labelrotation',
1081+
'labelrotation', 'rotation_mode',
10761082
*_gridline_param_names]
10771083

10781084
keymap = {
@@ -1090,10 +1096,19 @@ def _translate_tick_params(kw, reverse=False):
10901096
'labeltop': 'label2On',
10911097
}
10921098
if reverse:
1093-
kwtrans = {
1094-
oldkey: kw_.pop(newkey)
1095-
for oldkey, newkey in keymap.items() if newkey in kw_
1096-
}
1099+
kwtrans = {}
1100+
for oldkey, newkey in keymap.items():
1101+
if newkey in kw_:
1102+
if is_x_axis and newkey == 'label1On':
1103+
kwtrans['labelbottom'] = kw_.pop(newkey)
1104+
elif is_x_axis and newkey == 'tick1On':
1105+
kwtrans['bottom'] = kw_.pop(newkey)
1106+
elif is_x_axis and newkey == 'label2On':
1107+
kwtrans['labeltop'] = kw_.pop(newkey)
1108+
elif is_x_axis and newkey == 'tick2On':
1109+
kwtrans['top'] = kw_.pop(newkey)
1110+
else:
1111+
kwtrans[oldkey] = kw_.pop(newkey)
10971112
else:
10981113
kwtrans = {
10991114
newkey: kw_.pop(oldkey)
@@ -2095,6 +2110,8 @@ def set_ticklabels(self, labels, *, minor=False, fontdict=None, **kwargs):
20952110
for pos, (loc, tick) in enumerate(zip(locs, ticks)):
20962111
tick.update_position(loc)
20972112
tick_label = formatter(loc, pos)
2113+
tick.label1.axes = self.axes
2114+
tick.label2.axes = self.axes
20982115
# deal with label1
20992116
tick.label1.set_text(tick_label)
21002117
tick.label1._internal_update(kwargs)

lib/matplotlib/tests/test_axis.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import numpy as np
2-
32
import matplotlib.pyplot as plt
4-
from matplotlib.axis import XTick
3+
from matplotlib.axis import XTick, XAxis
54

65

76
def test_tick_labelcolor_array():
@@ -29,3 +28,14 @@ def test_axis_not_in_layout():
2928
# Positions should not be affected by overlapping 100 label
3029
assert ax1_left.get_position().bounds == ax2_left.get_position().bounds
3130
assert ax1_right.get_position().bounds == ax2_right.get_position().bounds
31+
32+
33+
def test__translate_tick_params():
34+
# Checks if _translat_tick_params returns the expected reverse
35+
# mapping when is_x_axis = True
36+
fig, ax = plt.subplots()
37+
xaxis = XAxis(ax)
38+
kw = {'label1On': 'dummy_string_1', 'label2On': 'dummy_string_2'}
39+
result = xaxis._translate_tick_params(kw, reverse=True, is_x_axis=True)
40+
assert result['labelbottom'] == 'dummy_string_1'
41+
assert result['labeltop'] == 'dummy_string_2'

lib/matplotlib/tests/test_text.py

+39
Original file line numberDiff line numberDiff line change
@@ -1135,3 +1135,42 @@ def test_font_wrap():
11351135
plt.text(3, 4, t, family='monospace', ha='right', wrap=True)
11361136
plt.text(-1, 0, t, fontsize=14, style='italic', ha='left', rotation=-15,
11371137
wrap=True)
1138+
1139+
1140+
def _test_ha_for_angle():
1141+
text_instance = Text()
1142+
angles = np.arange(0, 360.1, 0.1)
1143+
for angle in angles:
1144+
alignment = text_instance.ha_for_angle(angle)
1145+
assert alignment in ['center', 'left', 'right']
1146+
1147+
1148+
def _test_va_for_angle():
1149+
text_instance = Text()
1150+
angles = np.arange(0, 360.1, 0.1)
1151+
for angle in angles:
1152+
alignment = text_instance.va_for_angle(angle)
1153+
assert alignment in ['center', 'top', 'baseline']
1154+
1155+
1156+
@image_comparison(baseline_images=['text_xtick_ytick_rotation_modes'],
1157+
remove_text=False, extensions=['png'], style='mpl20')
1158+
def test_xtick_ytick_rotation_modes():
1159+
def set_ticks(ax, angles):
1160+
ax.set_xticks(np.arange(10))
1161+
ax.set_yticks(np.arange(10))
1162+
ax.set_xticklabels(['L'] * 10)
1163+
ax.set_yticklabels(['L'] * 10)
1164+
ax.xaxis.set_tick_params(rotation_mode='xtick', labelsize=7)
1165+
ax.yaxis.set_tick_params(rotation_mode='ytick', labelsize=7)
1166+
for label, angle in zip(ax.get_xticklabels(), angles):
1167+
label.set_rotation(angle)
1168+
for label, angle in zip(ax.get_yticklabels(), angles):
1169+
label.set_rotation(angle)
1170+
angles = np.linspace(0, 360, 10)
1171+
fig, axs = plt.subplots(1, 2, figsize=(5, 2.5))
1172+
set_ticks(axs[0], angles)
1173+
axs[1].xaxis.tick_top()
1174+
axs[1].yaxis.tick_right()
1175+
set_ticks(axs[1], angles)
1176+
plt.tight_layout()

lib/matplotlib/text.py

+40-5
Original file line numberDiff line numberDiff line change
@@ -301,16 +301,16 @@ def set_rotation_mode(self, m):
301301
302302
Parameters
303303
----------
304-
m : {None, 'default', 'anchor'}
304+
m : {None, 'default', 'anchor', 'xtick', 'ytick'}
305305
If ``"default"``, the text will be first rotated, then aligned according
306-
to their horizontal and vertical alignments. If ``"anchor"``, then
307-
alignment occurs before rotation. Passing ``None`` will set the rotation
308-
mode to ``"default"``.
306+
to their horizontal and vertical alignments. If ``"anchor"``, ``"xtick"``
307+
or ``"ytick", then alignment occurs before rotation. Passing ``None`` will
308+
set the rotation mode to ``"default"``.
309309
"""
310310
if m is None:
311311
m = "default"
312312
else:
313-
_api.check_in_list(("anchor", "default"), rotation_mode=m)
313+
_api.check_in_list(("anchor", "default", "xtick", "ytick"), rotation_mode=m)
314314
self._rotation_mode = m
315315
self.stale = True
316316

@@ -453,7 +453,18 @@ def _get_layout(self, renderer):
453453
valign = self._verticalalignment
454454

455455
rotation_mode = self.get_rotation_mode()
456+
ax = getattr(self, 'axes', None)
457+
if ax:
458+
y_tick_params = ax.yaxis.get_tick_params()
459+
is_tick_right_enabled = y_tick_params.get('labelright', False)
460+
x_tick_params = ax.xaxis.get_tick_params()
461+
is_tick_top_enabled = x_tick_params.get('labeltop', False)
456462
if rotation_mode != "anchor":
463+
angle = self.get_rotation()
464+
if rotation_mode == 'xtick':
465+
halign = self._ha_for_angle(angle, is_tick_top_enabled)
466+
elif rotation_mode == 'ytick':
467+
valign = self._va_for_angle(angle, is_tick_right_enabled)
457468
# compute the text location in display coords and the offsets
458469
# necessary to align the bbox with that location
459470
if halign == 'center':
@@ -1380,6 +1391,30 @@ def set_fontname(self, fontname):
13801391
"""
13811392
self.set_fontfamily(fontname)
13821393

1394+
def _ha_for_angle(self, angle, is_tick_top_enabled):
1395+
"""
1396+
Determines horizontal alignment ('ha') based on the angle of rotation
1397+
in degrees. Adjusts for is_tick_top_enabled.
1398+
"""
1399+
if (angle < 5 or 85 <= angle < 105 or 355 <= angle < 360 or
1400+
170 <= angle < 190 or 265 <= angle < 275):
1401+
return 'center'
1402+
elif 5 <= angle < 85 or 190 <= angle < 265:
1403+
return 'left' if is_tick_top_enabled else 'right'
1404+
return 'right' if is_tick_top_enabled else 'left'
1405+
1406+
def _va_for_angle(self, angle, is_tick_right_enabled):
1407+
"""
1408+
Determines vertical alignment ('va') based on the angle of rotation
1409+
in degrees. Adjusts for is_tick_right_enabled.
1410+
"""
1411+
if (angle < 5 or 355 <= angle < 360 or 170 <= angle < 190
1412+
or 85 <= angle < 105 or 265 <= angle < 275):
1413+
return 'center'
1414+
elif 190 <= angle < 265 or 5 <= angle < 85:
1415+
return 'baseline' if is_tick_right_enabled else 'top'
1416+
return 'top' if is_tick_right_enabled else 'baseline'
1417+
13831418

13841419
class OffsetFrom:
13851420
"""Callable helper class for working with `Annotation`."""

lib/matplotlib/text.pyi

+2
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ class Text(Artist):
106106
def set_fontname(self, fontname: str | Iterable[str]) -> None: ...
107107
def get_antialiased(self) -> bool: ...
108108
def set_antialiased(self, antialiased: bool) -> None: ...
109+
def _ha_for_angle(self, angle: Any, is_tick_top_enabled: bool) -> Literal['center', 'right', 'left'] | None: ...
110+
def _va_for_angle(self, angle: Any, is_tick_right_enabled: bool) -> Literal['center', 'top', 'baseline'] | None: ...
109111

110112
class OffsetFrom:
111113
def __init__(

0 commit comments

Comments
 (0)