Skip to content

ENH: Add tick label alignment parameters to tick_params #30009

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions doc/api/axis_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -272,3 +272,39 @@ specify a matching series of labels. Calling ``set_ticks`` makes a
Tick.set_pad
Tick.set_url
Tick.update_position

Tick Parameters
--------------

The following parameters can be used to control the appearance of tick marks and tick labels:

* ``direction`` : {'in', 'out', 'inout'}
Puts ticks inside the axes, outside the axes, or both.
* ``length`` : float
Tick length in points.
* ``width`` : float
Tick width in points.
* ``color`` : color
Tick color.
* ``pad`` : float
Distance in points between tick and label.
* ``labelsize`` : float or str
Tick label font size in points or as a string (e.g., 'large').
* ``labelcolor`` : color
Tick label color.
* ``labelfontfamily`` : str
Tick label font family.
* ``labelhorizontalalignment`` : {'left', 'center', 'right'}
Horizontal alignment of tick labels. Only applies when axis='x' or axis='y'.
* ``labelverticalalignment`` : {'top', 'center', 'bottom', 'baseline', 'center_baseline'}
Vertical alignment of tick labels. Only applies when axis='x' or axis='y'.
* ``labelrotation`` : float
Tick label rotation angle in degrees.
* ``grid_color`` : color
Gridline color.
* ``grid_alpha`` : float
Transparency of gridlines: 0 (transparent) to 1 (opaque).
* ``grid_linewidth`` : float
Width of gridlines in points.
* ``grid_linestyle`` : str
Any valid Line2D line style spec.
15 changes: 15 additions & 0 deletions doc/users/next_whats_new/tick_label_alignment.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Added tick label alignment parameters to tick_params
----------------------------------------

The ``tick_params`` method now supports setting the horizontal and vertical alignment
of tick labels using the ``labelhorizontalalignment`` and ``labelverticalalignment``
parameters. These parameters can be used when specifying a single axis ('x' or 'y')
to control the alignment of tick labels:

.. code-block:: python

ax.tick_params(axis='x', labelhorizontalalignment='right')
ax.tick_params(axis='y', labelverticalalignment='top')

This provides more control over tick label positioning and can be useful for
improving the readability and appearance of plots.
19 changes: 19 additions & 0 deletions lib/matplotlib/axes/_base.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# TEMP TEST: confirming file is tracked for commit

from collections.abc import Iterable, Sequence
from contextlib import ExitStack
import functools
Expand Down Expand Up @@ -3510,6 +3512,10 @@ def tick_params(self, axis='both', **kwargs):
Width of gridlines in points.
grid_linestyle : str
Any valid `.Line2D` line style spec.
labelhorizontalalignment : str
Horizontal alignment of tick labels. Only applies when axis='x' or axis='y'.
labelverticalalignment : str
Vertical alignment of tick labels. Only applies when axis='x' or axis='y'.

Examples
--------
Expand All @@ -3522,8 +3528,18 @@ def tick_params(self, axis='both', **kwargs):
and with dimensions 6 points by 2 points. Tick labels will
also be red. Gridlines will be red and translucent.

::

ax.tick_params(axis='x', labelhorizontalalignment='right',
labelverticalalignment='top')

This will align x-axis tick labels to the right horizontally and top vertically.
"""
_api.check_in_list(['x', 'y', 'both'], axis=axis)

# Store the axis parameter for validation in Axis.set_tick_params
self._tick_params_axis = axis

if axis in ['x', 'both']:
xkw = dict(kwargs)
xkw.pop('left', None)
Expand All @@ -3539,6 +3555,9 @@ def tick_params(self, axis='both', **kwargs):
ykw.pop('labelbottom', None)
self.yaxis.set_tick_params(**ykw)

# Clean up after validation
self._tick_params_axis = None

def set_axis_off(self):
"""
Hide all visual components of the x- and y-axis.
Expand Down
157 changes: 94 additions & 63 deletions lib/matplotlib/axis.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,52 +286,76 @@
raise NotImplementedError('Derived must override')

def _apply_params(self, **kwargs):
"""Apply the parameters to the tick and label."""
for name, target in [("gridOn", self.gridline),
("tick1On", self.tick1line),
("tick2On", self.tick2line),
("label1On", self.label1),
("label2On", self.label2)]:
("tick1On", self.tick1line),
("tick2On", self.tick2line),
("label1On", self.label1),
("label2On", self.label2)]:
if name in kwargs:
target.set_visible(kwargs.pop(name))

# Handle label alignment parameters
if 'labelhorizontalalignment' in kwargs:
halign = kwargs.pop('labelhorizontalalignment')
self.label1.set_horizontalalignment(halign)
self.label2.set_horizontalalignment(halign)

Check warning on line 302 in lib/matplotlib/axis.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/axis.py#L300-L302

Added lines #L300 - L302 were not covered by tests

if 'labelverticalalignment' in kwargs:
valign = kwargs.pop('labelverticalalignment')
self.label1.set_verticalalignment(valign)
self.label2.set_verticalalignment(valign)

Check warning on line 307 in lib/matplotlib/axis.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/axis.py#L305-L307

Added lines #L305 - L307 were not covered by tests

if any(k in kwargs for k in ['size', 'width', 'pad', 'tickdir']):
self._size = kwargs.pop('size', self._size)
# Width could be handled outside this block, but it is
# convenient to leave it here.
self._width = kwargs.pop('width', self._width)
self._base_pad = kwargs.pop('pad', self._base_pad)
# _apply_tickdir uses _size and _base_pad to make _pad, and also
# sets the ticklines markers.
self._apply_tickdir(kwargs.pop('tickdir', self._tickdir))
for line in (self.tick1line, self.tick2line):
line.set_markersize(self._size)
line.set_markeredgewidth(self._width)
# _get_text1_transform uses _pad from _apply_tickdir.
trans = self._get_text1_transform()[0]
self.label1.set_transform(trans)
trans = self._get_text2_transform()[0]
self.label2.set_transform(trans)
self._tickdir = kwargs.pop('tickdir', self._tickdir)
self._apply_tickdir()

tick_kw = {k: v for k, v in kwargs.items() if k in ['color', 'zorder']}
if 'color' in kwargs:
tick_kw['markeredgecolor'] = kwargs['color']
self.tick1line.set(**tick_kw)
self.tick2line.set(**tick_kw)
for k, v in tick_kw.items():
setattr(self, '_' + k, v)
if tick_kw:
self.tick1line.set(**tick_kw)
self.tick2line.set(**tick_kw)

Check warning on line 321 in lib/matplotlib/axis.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/axis.py#L320-L321

Added lines #L320 - L321 were not covered by tests

tick_kw = {k: v for k, v in kwargs.items()

Check warning on line 323 in lib/matplotlib/axis.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/axis.py#L323

Added line #L323 was not covered by tests
if k in ['color', 'zorder', 'fontsize', 'fontfamily']}
if tick_kw:
self.label1.set(**tick_kw)
self.label2.set(**tick_kw)

Check warning on line 327 in lib/matplotlib/axis.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/axis.py#L326-L327

Added lines #L326 - L327 were not covered by tests

if 'labelcolor' in kwargs:
color = kwargs.pop('labelcolor')
self.label1.set_color(color)
self.label2.set_color(color)

Check warning on line 332 in lib/matplotlib/axis.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/axis.py#L330-L332

Added lines #L330 - L332 were not covered by tests

if 'labelrotation' in kwargs:
self._set_labelrotation(kwargs.pop('labelrotation'))
self.label1.set(rotation=self._labelrotation[1])
self.label2.set(rotation=self._labelrotation[1])
rotation = kwargs.pop('labelrotation')
self.label1.set_rotation(rotation)
self.label2.set_rotation(rotation)

Check warning on line 337 in lib/matplotlib/axis.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/axis.py#L335-L337

Added lines #L335 - L337 were not covered by tests

label_kw = {k[5:]: v for k, v in kwargs.items()
if k in ['labelsize', 'labelcolor', 'labelfontfamily',
'labelrotation_mode']}
self.label1.set(**label_kw)
self.label2.set(**label_kw)
if 'labelrotation_mode' in kwargs:
rotation_mode = kwargs.pop('labelrotation_mode')
self.label1.set_rotation_mode(rotation_mode)
self.label2.set_rotation_mode(rotation_mode)

Check warning on line 342 in lib/matplotlib/axis.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/axis.py#L340-L342

Added lines #L340 - L342 were not covered by tests

grid_kw = {k[5:]: v for k, v in kwargs.items()
if k in _gridline_param_names}
self.gridline.set(**grid_kw)
if 'grid_color' in kwargs:
self.gridline.set_color(kwargs.pop('grid_color'))

Check warning on line 345 in lib/matplotlib/axis.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/axis.py#L345

Added line #L345 was not covered by tests

if 'grid_alpha' in kwargs:
self.gridline.set_alpha(kwargs.pop('grid_alpha'))

Check warning on line 348 in lib/matplotlib/axis.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/axis.py#L348

Added line #L348 was not covered by tests

if 'grid_linewidth' in kwargs:
self.gridline.set_linewidth(kwargs.pop('grid_linewidth'))

Check warning on line 351 in lib/matplotlib/axis.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/axis.py#L351

Added line #L351 was not covered by tests

if 'grid_linestyle' in kwargs:
self.gridline.set_linestyle(kwargs.pop('grid_linestyle'))

Check warning on line 354 in lib/matplotlib/axis.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/axis.py#L354

Added line #L354 was not covered by tests

if kwargs:
_api.warn_external(f"The following kwargs were not used by contour: "

Check warning on line 357 in lib/matplotlib/axis.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/axis.py#L357

Added line #L357 was not covered by tests
f"{kwargs}")

def update_position(self, loc):
"""Set the location of tick in data coords with scalar *loc*."""
Expand Down Expand Up @@ -930,48 +954,52 @@
"""Remove minor ticks from the Axis."""
self.set_minor_locator(mticker.NullLocator())

def set_tick_params(self, which='major', reset=False, **kwargs):
def set_tick_params(self, which='major', reset=False, **kw):
"""
Set appearance parameters for ticks, ticklabels, and gridlines.

For documentation of keyword arguments, see
:meth:`matplotlib.axes.Axes.tick_params`.

See Also
--------
.Axis.get_tick_params
View the current style settings for ticks, ticklabels, and
gridlines.
Parameters
----------
which : {'major', 'minor', 'both'}, default: 'major'
The group of ticks to which the parameters are applied.
reset : bool, default: False
Whether to reset the ticks to defaults before updating them.
**kw
Tick properties to set.
"""
_api.check_in_list(['major', 'minor', 'both'], which=which)
kwtrans = self._translate_tick_params(kwargs)

# the kwargs are stored in self._major/minor_tick_kw so that any
# future new ticks will automatically get them
# Get the axis from the parent Axes if available
axis = getattr(self.axes, '_tick_params_axis', None)

# Validate alignment parameters based on axis
if axis == 'x' and 'labelhorizontalalignment' in kw:
_api.check_in_list(['left', 'center', 'right'],

Check failure on line 980 in lib/matplotlib/axis.py

View workflow job for this annotation

GitHub Actions / ruff

[rdjson] reported by reviewdog 🐶 Trailing whitespace Raw Output: message:"Trailing whitespace" location:{path:"/home/runner/work/matplotlib/matplotlib/lib/matplotlib/axis.py" range:{start:{line:980 column:60} end:{line:980 column:61}}} source:{name:"ruff" url:"https://docs.astral.sh/ruff"} code:{value:"W291" url:"https://docs.astral.sh/ruff/rules/trailing-whitespace"} suggestions:{range:{start:{line:980 column:60} end:{line:980 column:61}}}
labelhorizontalalignment=kw['labelhorizontalalignment'])
elif axis == 'y' and 'labelhorizontalalignment' in kw:
_api.check_in_list(['left', 'center', 'right'],

Check failure on line 983 in lib/matplotlib/axis.py

View workflow job for this annotation

GitHub Actions / ruff

[rdjson] reported by reviewdog 🐶 Trailing whitespace Raw Output: message:"Trailing whitespace" location:{path:"/home/runner/work/matplotlib/matplotlib/lib/matplotlib/axis.py" range:{start:{line:983 column:60} end:{line:983 column:61}}} source:{name:"ruff" url:"https://docs.astral.sh/ruff"} code:{value:"W291" url:"https://docs.astral.sh/ruff/rules/trailing-whitespace"} suggestions:{range:{start:{line:983 column:60} end:{line:983 column:61}}}

Check warning on line 983 in lib/matplotlib/axis.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/axis.py#L983

Added line #L983 was not covered by tests
labelhorizontalalignment=kw['labelhorizontalalignment'])

if axis == 'x' and 'labelverticalalignment' in kw:
_api.check_in_list(['top', 'center', 'bottom', 'baseline', 'center_baseline'],

Check failure on line 987 in lib/matplotlib/axis.py

View workflow job for this annotation

GitHub Actions / ruff

[rdjson] reported by reviewdog 🐶 Line too long (91 > 88) Raw Output: message:"Line too long (91 > 88)" location:{path:"/home/runner/work/matplotlib/matplotlib/lib/matplotlib/axis.py" range:{start:{line:987 column:89} end:{line:987 column:92}}} source:{name:"ruff" url:"https://docs.astral.sh/ruff"} code:{value:"E501" url:"https://docs.astral.sh/ruff/rules/line-too-long"}

Check failure on line 987 in lib/matplotlib/axis.py

View workflow job for this annotation

GitHub Actions / ruff

[rdjson] reported by reviewdog 🐶 Trailing whitespace Raw Output: message:"Trailing whitespace" location:{path:"/home/runner/work/matplotlib/matplotlib/lib/matplotlib/axis.py" range:{start:{line:987 column:91} end:{line:987 column:92}}} source:{name:"ruff" url:"https://docs.astral.sh/ruff"} code:{value:"W291" url:"https://docs.astral.sh/ruff/rules/trailing-whitespace"} suggestions:{range:{start:{line:987 column:91} end:{line:987 column:92}}}

Check warning on line 987 in lib/matplotlib/axis.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/axis.py#L987

Added line #L987 was not covered by tests
labelverticalalignment=kw['labelverticalalignment'])
elif axis == 'y' and 'labelverticalalignment' in kw:
_api.check_in_list(['top', 'center', 'bottom', 'baseline', 'center_baseline'],

Check failure on line 990 in lib/matplotlib/axis.py

View workflow job for this annotation

GitHub Actions / ruff

[rdjson] reported by reviewdog 🐶 Line too long (91 > 88) Raw Output: message:"Line too long (91 > 88)" location:{path:"/home/runner/work/matplotlib/matplotlib/lib/matplotlib/axis.py" range:{start:{line:990 column:89} end:{line:990 column:92}}} source:{name:"ruff" url:"https://docs.astral.sh/ruff"} code:{value:"E501" url:"https://docs.astral.sh/ruff/rules/line-too-long"}

Check failure on line 990 in lib/matplotlib/axis.py

View workflow job for this annotation

GitHub Actions / ruff

[rdjson] reported by reviewdog 🐶 Trailing whitespace Raw Output: message:"Trailing whitespace" location:{path:"/home/runner/work/matplotlib/matplotlib/lib/matplotlib/axis.py" range:{start:{line:990 column:91} end:{line:990 column:92}}} source:{name:"ruff" url:"https://docs.astral.sh/ruff"} code:{value:"W291" url:"https://docs.astral.sh/ruff/rules/trailing-whitespace"} suggestions:{range:{start:{line:990 column:91} end:{line:990 column:92}}}

Check warning on line 990 in lib/matplotlib/axis.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/axis.py#L990

Added line #L990 was not covered by tests
labelverticalalignment=kw['labelverticalalignment'])

kwtrans = self._translate_tick_params(kw)

if reset:
if which in ['major', 'both']:
self._reset_major_tick_kw()
self._major_tick_kw.update(kwtrans)
if which in ['minor', 'both']:
self._reset_minor_tick_kw()
self._minor_tick_kw.update(kwtrans)
self.reset_ticks()
else:
if which in ['major', 'both']:
self._major_tick_kw.update(kwtrans)
for tick in self.majorTicks:
tick._apply_params(**kwtrans)
if which in ['minor', 'both']:
self._minor_tick_kw.update(kwtrans)
for tick in self.minorTicks:
tick._apply_params(**kwtrans)
# labelOn and labelcolor also apply to the offset text.
if 'label1On' in kwtrans or 'label2On' in kwtrans:
self.offsetText.set_visible(
self._major_tick_kw.get('label1On', False)
or self._major_tick_kw.get('label2On', False))
if 'labelcolor' in kwtrans:
self.offsetText.set_color(kwtrans['labelcolor'])
if which in ['major', 'both']:
for key, val in kwtrans.items():
setattr(self.major, key, val)
if which in ['minor', 'both']:
for key, val in kwtrans.items():
setattr(self.minor, key, val)

self.stale = True

Expand Down Expand Up @@ -1055,6 +1083,7 @@
'length', 'direction', 'left', 'bottom', 'right', 'top',
'labelleft', 'labelbottom', 'labelright', 'labeltop',
'labelrotation', 'labelrotation_mode',
'labelhorizontalalignment', 'labelverticalalignment',
*_gridline_param_names]

keymap = {
Expand All @@ -1071,6 +1100,8 @@
'labelbottom': 'label1On',
'labelright': 'label2On',
'labeltop': 'label2On',
'labelhorizontalalignment': 'labelhorizontalalignment',
'labelverticalalignment': 'labelverticalalignment',
}
if reverse:
kwtrans = {}
Expand Down
26 changes: 26 additions & 0 deletions lib/matplotlib/tests/test_axis.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import numpy as np
import pytest

import matplotlib.pyplot as plt
from matplotlib.axis import XTick
Expand Down Expand Up @@ -67,3 +68,28 @@
right=True, labelright=True, left=False, labelleft=False)
assert ax.xaxis.get_ticks_position() == "top"
assert ax.yaxis.get_ticks_position() == "right"


def test_tick_label_alignment():
"""Test that tick label alignment can be set via tick_params."""
fig, ax = plt.subplots()

Check failure on line 76 in lib/matplotlib/tests/test_axis.py

View workflow job for this annotation

GitHub Actions / ruff

[rdjson] reported by reviewdog 🐶 Blank line contains whitespace Raw Output: message:"Blank line contains whitespace" location:{path:"/home/runner/work/matplotlib/matplotlib/lib/matplotlib/tests/test_axis.py" range:{start:{line:76 column:1} end:{line:76 column:5}}} source:{name:"ruff" url:"https://docs.astral.sh/ruff"} code:{value:"W293" url:"https://docs.astral.sh/ruff/rules/blank-line-with-whitespace"} suggestions:{range:{start:{line:76 column:1} end:{line:76 column:5}}}
ax.tick_params(axis='x', labelhorizontalalignment='right')
assert ax.xaxis.get_major_ticks()[0].label1.get_horizontalalignment() == 'right'

Check failure on line 79 in lib/matplotlib/tests/test_axis.py

View workflow job for this annotation

GitHub Actions / ruff

[rdjson] reported by reviewdog 🐶 Blank line contains whitespace Raw Output: message:"Blank line contains whitespace" location:{path:"/home/runner/work/matplotlib/matplotlib/lib/matplotlib/tests/test_axis.py" range:{start:{line:79 column:1} end:{line:79 column:5}}} source:{name:"ruff" url:"https://docs.astral.sh/ruff"} code:{value:"W293" url:"https://docs.astral.sh/ruff/rules/blank-line-with-whitespace"} suggestions:{range:{start:{line:79 column:1} end:{line:79 column:5}}}
ax.tick_params(axis='x', labelverticalalignment='top')
assert ax.xaxis.get_major_ticks()[0].label1.get_verticalalignment() == 'top'

Check warning on line 81 in lib/matplotlib/tests/test_axis.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/tests/test_axis.py#L80-L81

Added lines #L80 - L81 were not covered by tests

Check failure on line 82 in lib/matplotlib/tests/test_axis.py

View workflow job for this annotation

GitHub Actions / ruff

[rdjson] reported by reviewdog 🐶 Blank line contains whitespace Raw Output: message:"Blank line contains whitespace" location:{path:"/home/runner/work/matplotlib/matplotlib/lib/matplotlib/tests/test_axis.py" range:{start:{line:82 column:1} end:{line:82 column:5}}} source:{name:"ruff" url:"https://docs.astral.sh/ruff"} code:{value:"W293" url:"https://docs.astral.sh/ruff/rules/blank-line-with-whitespace"} suggestions:{range:{start:{line:82 column:1} end:{line:82 column:5}}}
# Test horizontal alignment for y-axis
ax.tick_params(axis='y', labelhorizontalalignment='left')
assert ax.yaxis.get_major_ticks()[0].label1.get_horizontalalignment() == 'left'

Check warning on line 85 in lib/matplotlib/tests/test_axis.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/tests/test_axis.py#L84-L85

Added lines #L84 - L85 were not covered by tests

# Test vertical alignment for y-axis
ax.tick_params(axis='y', labelverticalalignment='bottom')
assert ax.yaxis.get_major_ticks()[0].label1.get_verticalalignment() == 'bottom'

Check warning on line 89 in lib/matplotlib/tests/test_axis.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/tests/test_axis.py#L88-L89

Added lines #L88 - L89 were not covered by tests

# Test that alignment parameters are validated
with pytest.raises(ValueError):
ax.tick_params(axis='x', labelhorizontalalignment='invalid')
with pytest.raises(ValueError):
ax.tick_params(axis='y', labelverticalalignment='invalid')

Check warning on line 95 in lib/matplotlib/tests/test_axis.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/tests/test_axis.py#L92-L95

Added lines #L92 - L95 were not covered by tests
Loading