diff --git a/doc/api/axis_api.rst b/doc/api/axis_api.rst index 85ba990ffece..c1176b9bc5db 100644 --- a/doc/api/axis_api.rst +++ b/doc/api/axis_api.rst @@ -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. diff --git a/doc/users/next_whats_new/tick_label_alignment.rst b/doc/users/next_whats_new/tick_label_alignment.rst new file mode 100644 index 000000000000..92e32dfece63 --- /dev/null +++ b/doc/users/next_whats_new/tick_label_alignment.rst @@ -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. \ No newline at end of file diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 15f8e97b449f..dc3a3cadbe53 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -1,3 +1,5 @@ +# TEMP TEST: confirming file is tracked for commit + from collections.abc import Iterable, Sequence from contextlib import ExitStack import functools @@ -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 -------- @@ -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) @@ -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. diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 19096fc29d3e..ba03373e1205 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -286,52 +286,76 @@ def get_view_interval(self): 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) + + if 'labelverticalalignment' in kwargs: + valign = kwargs.pop('labelverticalalignment') + self.label1.set_verticalalignment(valign) + self.label2.set_verticalalignment(valign) + 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) + + tick_kw = {k: v for k, v in kwargs.items() + if k in ['color', 'zorder', 'fontsize', 'fontfamily']} + if tick_kw: + self.label1.set(**tick_kw) + self.label2.set(**tick_kw) + + if 'labelcolor' in kwargs: + color = kwargs.pop('labelcolor') + self.label1.set_color(color) + self.label2.set_color(color) 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) - 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) - 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')) + + if 'grid_alpha' in kwargs: + self.gridline.set_alpha(kwargs.pop('grid_alpha')) + + if 'grid_linewidth' in kwargs: + self.gridline.set_linewidth(kwargs.pop('grid_linewidth')) + + if 'grid_linestyle' in kwargs: + self.gridline.set_linestyle(kwargs.pop('grid_linestyle')) + + if kwargs: + _api.warn_external(f"The following kwargs were not used by contour: " + f"{kwargs}") def update_position(self, loc): """Set the location of tick in data coords with scalar *loc*.""" @@ -930,48 +954,52 @@ def minorticks_off(self): """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'], + labelhorizontalalignment=kw['labelhorizontalalignment']) + elif axis == 'y' and 'labelhorizontalalignment' in kw: + _api.check_in_list(['left', 'center', 'right'], + labelhorizontalalignment=kw['labelhorizontalalignment']) + + if axis == 'x' and 'labelverticalalignment' in kw: + _api.check_in_list(['top', 'center', 'bottom', 'baseline', 'center_baseline'], + labelverticalalignment=kw['labelverticalalignment']) + elif axis == 'y' and 'labelverticalalignment' in kw: + _api.check_in_list(['top', 'center', 'bottom', 'baseline', 'center_baseline'], + 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 @@ -1055,6 +1083,7 @@ def _translate_tick_params(cls, kw, reverse=False): 'length', 'direction', 'left', 'bottom', 'right', 'top', 'labelleft', 'labelbottom', 'labelright', 'labeltop', 'labelrotation', 'labelrotation_mode', + 'labelhorizontalalignment', 'labelverticalalignment', *_gridline_param_names] keymap = { @@ -1071,6 +1100,8 @@ def _translate_tick_params(cls, kw, reverse=False): 'labelbottom': 'label1On', 'labelright': 'label2On', 'labeltop': 'label2On', + 'labelhorizontalalignment': 'labelhorizontalalignment', + 'labelverticalalignment': 'labelverticalalignment', } if reverse: kwtrans = {} diff --git a/lib/matplotlib/tests/test_axis.py b/lib/matplotlib/tests/test_axis.py index e33656ea9c17..c268ba815937 100644 --- a/lib/matplotlib/tests/test_axis.py +++ b/lib/matplotlib/tests/test_axis.py @@ -1,4 +1,5 @@ import numpy as np +import pytest import matplotlib.pyplot as plt from matplotlib.axis import XTick @@ -67,3 +68,28 @@ def test_get_tick_position_tick_params(): 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() + + ax.tick_params(axis='x', labelhorizontalalignment='right') + assert ax.xaxis.get_major_ticks()[0].label1.get_horizontalalignment() == 'right' + + ax.tick_params(axis='x', labelverticalalignment='top') + assert ax.xaxis.get_major_ticks()[0].label1.get_verticalalignment() == 'top' + + # Test horizontal alignment for y-axis + ax.tick_params(axis='y', labelhorizontalalignment='left') + assert ax.yaxis.get_major_ticks()[0].label1.get_horizontalalignment() == 'left' + + # Test vertical alignment for y-axis + ax.tick_params(axis='y', labelverticalalignment='bottom') + assert ax.yaxis.get_major_ticks()[0].label1.get_verticalalignment() == 'bottom' + + # 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')