Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 249bb58

Browse files
committed
Implement xtick and ytick rotation_mode
1 parent 0439b37 commit 249bb58

File tree

9 files changed

+158
-36
lines changed

9 files changed

+158
-36
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
``xtick`` and ``ytick`` rotation modes
2+
--------------------------------------
3+
4+
A new feature has been added for handling rotation of xtick and ytick
5+
labels more intuitively. The new rotation modes automatically adjusts the
6+
alignment of rotated tick labels. This applies to tick labels on all four
7+
sides of the plot (bottom, top, left, right), reducing the need for manual
8+
adjustments when rotating labels.
9+
10+
.. plot::
11+
:include-source: true
12+
:alt: Example of rotated xtick and ytick labels.
13+
14+
import matplotlib.pyplot as plt
15+
16+
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))
17+
18+
s = range(5)
19+
ax1.set_xticks(s)
20+
ax1.set_xticklabels(['label'] * 5, rotation=-45, rotation_mode='xtick')
21+
ax1.set_yticks(s)
22+
ax1.set_yticklabels(['label'] * 5, rotation=45, rotation_mode='ytick')
23+
ax2.set_xticks(s)
24+
ax2.set_xticklabels(['label'] * 5, rotation=-45, rotation_mode='xtick')
25+
ax2.xaxis.tick_top()
26+
ax2.set_yticks(s)
27+
ax2.set_yticklabels(['label'] * 5, rotation=45, rotation_mode='ytick')
28+
ax2.yaxis.tick_right()
29+
30+
plt.tight_layout()
31+
plt.show()

‎galleries/examples/images_contours_and_fields/image_annotated_heatmap.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464

6565
# Show all ticks and label them with the respective list entries
6666
ax.set_xticks(range(len(farmers)), labels=farmers,
67-
rotation=45, ha="right", rotation_mode="anchor")
67+
rotation=45, rotation_mode="xtick")
6868
ax.set_yticks(range(len(vegetables)), labels=vegetables)
6969

7070
# Loop over data dimensions and create text annotations.
@@ -135,7 +135,7 @@ def heatmap(data, row_labels, col_labels, ax=None,
135135

136136
# Show all ticks and label them with the respective list entries.
137137
ax.set_xticks(range(data.shape[1]), labels=col_labels,
138-
rotation=-30, ha="right", rotation_mode="anchor")
138+
rotation=-30, rotation_mode="xtick")
139139
ax.set_yticks(range(data.shape[0]), labels=row_labels)
140140

141141
# Let the horizontal axes labeling appear on top.

‎galleries/examples/subplots_axes_and_figures/align_labels_demo.py‎

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,19 @@
1717

1818
fig, axs = plt.subplots(2, 2, layout='constrained')
1919

20-
ax = axs[0][0]
21-
ax.plot(np.arange(0, 1e6, 1000))
22-
ax.set_title('Title0 0')
23-
ax.set_ylabel('YLabel0 0')
24-
25-
ax = axs[0][1]
26-
ax.plot(np.arange(1., 0., -0.1) * 2000., np.arange(1., 0., -0.1))
27-
ax.set_title('Title0 1')
28-
ax.xaxis.tick_top()
29-
ax.tick_params(axis='x', rotation=55)
30-
31-
3220
for i in range(2):
33-
ax = axs[1][i]
34-
ax.plot(np.arange(1., 0., -0.1) * 2000., np.arange(1., 0., -0.1))
35-
ax.set_ylabel('YLabel1 %d' % i)
36-
ax.set_xlabel('XLabel1 %d' % i)
37-
if i == 0:
38-
ax.tick_params(axis='x', rotation=55)
21+
for j in range(2):
22+
ax = axs[i][j]
23+
ax.plot(np.arange(1., 0., -0.1) * 1000., np.arange(1., 0., -0.1))
24+
ax.set_title(f'Title {i} {j}')
25+
ax.set_xlabel(f'XLabel {i} {j}')
26+
ax.set_ylabel(f'YLabel {i} {j}')
27+
if (i == 0 and j == 1) or (i == 1 and j == 0):
28+
if i == 0 and j == 1:
29+
ax.xaxis.tick_top()
30+
ax.set_xticks(np.linspace(0, 1000, 5))
31+
ax.set_xticklabels([250 * n for n in range(5)])
32+
ax.xaxis.set_tick_params(rotation=55, rotation_mode='xtick')
3933

4034
fig.align_labels() # same as fig.align_xlabels(); fig.align_ylabels()
4135
fig.align_titles()

‎lib/matplotlib/axis.py‎

Lines changed: 23 additions & 9 deletions
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')
@@ -1043,12 +1048,11 @@ def get_tick_params(self, which='major'):
10431048
_api.check_in_list(['major', 'minor'], which=which)
10441049
if which == 'major':
10451050
return self._translate_tick_params(
1046-
self._major_tick_kw, reverse=True
1047-
)
1051+
self._major_tick_kw, reverse=True)
10481052
return self._translate_tick_params(self._minor_tick_kw, reverse=True)
10491053

1050-
@staticmethod
1051-
def _translate_tick_params(kw, reverse=False):
1054+
@classmethod
1055+
def _translate_tick_params(cls, kw, reverse=False):
10521056
"""
10531057
Translate the kwargs supported by `.Axis.set_tick_params` to kwargs
10541058
supported by `.Tick._apply_params`.
@@ -1072,7 +1076,7 @@ def _translate_tick_params(kw, reverse=False):
10721076
'tick1On', 'tick2On', 'label1On', 'label2On',
10731077
'length', 'direction', 'left', 'bottom', 'right', 'top',
10741078
'labelleft', 'labelbottom', 'labelright', 'labeltop',
1075-
'labelrotation',
1079+
'labelrotation','rotation_mode',
10761080
*_gridline_param_names]
10771081

10781082
keymap = {
@@ -1089,11 +1093,21 @@ def _translate_tick_params(kw, reverse=False):
10891093
'labelright': 'label2On',
10901094
'labeltop': 'label2On',
10911095
}
1096+
is_x_axis = cls.axis_name == 'x'
10921097
if reverse:
1093-
kwtrans = {
1094-
oldkey: kw_.pop(newkey)
1095-
for oldkey, newkey in keymap.items() if newkey in kw_
1096-
}
1098+
kwtrans = {}
1099+
for oldkey, newkey in keymap.items():
1100+
if newkey in kw_:
1101+
if is_x_axis and newkey == 'label1On':
1102+
kwtrans['labelbottom'] = kw_.pop(newkey)
1103+
elif is_x_axis and newkey == 'tick1On':
1104+
kwtrans['bottom'] = kw_.pop(newkey)
1105+
elif is_x_axis and newkey == 'label2On':
1106+
kwtrans['labeltop'] = kw_.pop(newkey)
1107+
elif is_x_axis and newkey == 'tick2On':
1108+
kwtrans['top'] = kw_.pop(newkey)
1109+
else:
1110+
kwtrans[oldkey] = kw_.pop(newkey)
10971111
else:
10981112
kwtrans = {
10991113
newkey: kw_.pop(oldkey)
7.16 KB
Loading[フレーム]

‎lib/matplotlib/tests/test_axis.py‎

Lines changed: 13 additions & 2 deletions
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,15 @@ 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+
fig, ax = plt.subplots()
35+
xaxis = XAxis(ax)
36+
kw = {'label1On': 'dummy_string_1', 'label2On': 'dummy_string_2',
37+
'tick1On': 'dummy_string_3', 'tick2On': 'dummy_string_4'}
38+
result = xaxis._translate_tick_params(kw, reverse=True)
39+
assert result['labelbottom'] == 'dummy_string_1'
40+
assert result['labeltop'] == 'dummy_string_2'
41+
assert result['bottom'] == 'dummy_string_3'
42+
assert result['top'] == 'dummy_string_4'

‎lib/matplotlib/tests/test_text.py‎

Lines changed: 39 additions & 0 deletions
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‎

Lines changed: 36 additions & 5 deletions
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

@@ -454,6 +454,11 @@ def _get_layout(self, renderer):
454454

455455
rotation_mode = self.get_rotation_mode()
456456
if rotation_mode != "anchor":
457+
angle = self.get_rotation()
458+
if rotation_mode == 'xtick':
459+
halign = self._ha_for_angle(angle)
460+
elif rotation_mode == 'ytick':
461+
valign = self._va_for_angle(angle)
457462
# compute the text location in display coords and the offsets
458463
# necessary to align the bbox with that location
459464
if halign == 'center':
@@ -1380,6 +1385,32 @@ def set_fontname(self, fontname):
13801385
"""
13811386
self.set_fontfamily(fontname)
13821387

1388+
def _ha_for_angle(self, angle):
1389+
"""
1390+
Determines horizontal alignment ('ha') for rotation_mode "xtick" based on
1391+
the angle of rotation in degrees and the vertical alignment.
1392+
"""
1393+
anchor_at_bottom = self.get_verticalalignment() == 'bottom'
1394+
if (angle < 5 or 85 <= angle < 105 or 355 <= angle < 360 or
1395+
170 <= angle < 190 or 265 <= angle < 275):
1396+
return 'center'
1397+
elif 5 <= angle < 85 or 190 <= angle < 265:
1398+
return 'left' if anchor_at_bottom else 'right'
1399+
return 'right' if anchor_at_bottom else 'left'
1400+
1401+
def _va_for_angle(self, angle):
1402+
"""
1403+
Determines vertical alignment ('va') based on the angle of rotation
1404+
in degrees. Adjusts for is_tick_right_enabled.
1405+
"""
1406+
anchor_at_left = self.get_horizontalalignment() == 'left'
1407+
if (angle < 5 or 355 <= angle < 360 or 170 <= angle < 190
1408+
or 85 <= angle < 105 or 265 <= angle < 275):
1409+
return 'center'
1410+
elif 190 <= angle < 265 or 5 <= angle < 85:
1411+
return 'baseline' if anchor_at_left else 'top'
1412+
return 'top' if anchor_at_left else 'baseline'
1413+
13831414

13841415
class OffsetFrom:
13851416
"""Callable helper class for working with `Annotation`."""

‎lib/matplotlib/text.pyi‎

Lines changed: 2 additions & 0 deletions
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) -> Literal['center', 'right', 'left'] | None: ...
110+
def _va_for_angle(self, angle: Any) -> Literal['center', 'top', 'baseline'] | None: ...
109111

110112
class OffsetFrom:
111113
def __init__(

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /