Back to Blog
Python

Python Matplotlib Ticks Rotation, Text and Annotations

python matplotlib ticks rotation text and annotations: Rotate matplotlib tick labels with tick_params, set explicit tick positions, and add text or arrow annotations t...

matplotlibdata-visualizationpython-plottingtick-labelsannotations
A matplotlib line chart with rotated x-axis tick labels, a corner text label, and a curved arrow annotation pointing to a data peak.

When category names or date labels overlap on a matplotlib axis, rotating the tick labels is the standard fix. The same axes object also provides text() for placing labels and annotate() for adding arrows with text. The practical pattern for python matplotlib ticks rotation text and annotations combines tick_params rotation, explicit tick positions, and annotation calls on one axes.

Rotating Tick Labels with tick_params

The most direct way to rotate tick labels is tick_params on the axes object:

import matplotlib.pyplot as plt fig, ax = plt.subplots() ax.bar(['Alpha', 'Beta', 'Gamma', 'Delta'], [4, 7, 2, 9]) ax.tick_params(axis='x', rotation=45) plt.show()

tick_params accepts rotation as a keyword argument and applies it to the tick labels on the specified axis. The axis parameter accepts 'x', 'y', or 'both'. This approach works with any axes that has ticks, including those returned by pandas plotting methods and seaborn.

You can combine rotation with other label properties in the same call:

ax.tick_params(axis='x', rotation=45, labelsize=9, labelcolor='#333333')

This keeps all tick styling in one place rather than scattering set_xticklabels calls through the code.

Setting Tick Positions and Labels Explicitly

When the default tick positions are not what you need, set them explicitly:

import numpy as np rng = np.random.default_rng(3) x = np.arange(12) y = rng.normal(size=12) fig, ax = plt.subplots() ax.plot(x, y, marker='o') ax.set_xticks(x) ax.set_xticklabels([f'row-{i}' for i in x], rotation=30, ha='right') plt.show()

set_xticks defines where the ticks appear, and set_xticklabels assigns the text. The rotation parameter rotates the labels, and ha='right' aligns the right edge of each label to its tick position. Without ha='right', rotated labels often appear shifted relative to their tick marks because the default horizontal alignment is 'center'.

For date axes, the same pattern applies, but you typically use a formatter instead of manual label strings:

import matplotlib.dates as mdates ax.xaxis.set_major_locator(mdates.DayLocator(interval=7)) ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d')) ax.tick_params(axis='x', rotation=45)

The formatter controls the text, and tick_params handles rotation.

Adding Text with ax.text()

ax.text(x, y, s) places a string at data coordinates:

ax.text(2, 0.5, 'threshold exceeded', color='red')

The default coordinate system is data coordinates. To place text relative to the axes, for example a caption in the corner, pass a transform:

ax.text(0.02, 0.98, 'normalized values', transform=ax.transAxes, va='top')

With transform=ax.transAxes, the coordinates range from 0 to 1 across the axes, independent of the data limits. va='top' keeps the text inside the axes when placed near the top edge.

ax.text does not draw an arrow or connector. It is the right choice when you only need a label at a fixed position.

Adding Annotations with ax.annotate()

ax.annotate extends text with an arrow and a second point:

ax.annotate('peak', xy=(x[5], y[5]), xytext=(x[5] + 1, y[5] + 0.5), arrowprops=dict(arrowstyle='->'))

xy is the data point being annotated, xytext is where the text is placed, and arrowprops draws the connector. The arrowstyle string controls the arrow shape; '->' gives a simple arrow, 'fancy' adds a curved arrowhead.

When the text and the target point are close, the arrow may overlap the text. Increase the offset between xytext and xy, or curve the connector:

ax.annotate('peak', xy=(x[5], y[5]), xytext=(x[5] + 1, y[5] + 0.5), arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0.2'))

annotate also accepts a transform parameter, so you can annotate in axes coordinates:

ax.annotate('start', xy=(0.1, 0.1), xytext=(0.3, 0.3), transform=ax.transAxes, arrowprops=dict(arrowstyle='->'))

Combining Rotation with Text and Annotations

A realistic figure often needs all three: rotated tick labels, a data label, and an annotation pointing to a feature.

import matplotlib.pyplot as plt import numpy as np rng = np.random.default_rng(7) x = np.arange(12) y = rng.normal(size=12) fig, ax = plt.subplots(figsize=(8, 4)) ax.plot(x, y, marker='o') ax.set_xticks(x) ax.set_xticklabels([f'Q{i % 4 + 1}-{2020 + i // 4}' for i in x], rotation=30, ha='right') peak = int(np.argmax(y)) ax.annotate(f'peak {y[peak]:.2f}', xy=(x[peak], y[peak]), xytext=(x[peak] + 1.2, y[peak] + 0.6), arrowprops=dict(arrowstyle='->')) ax.text(0.02, 0.95, 'quarterly returns', transform=ax.transAxes, va='top') fig.tight_layout() plt.show()

The rotated labels stay readable, the annotation points to the peak, and the axes-relative text labels the plot without interfering with the data range.

Alignment and Clipping Pitfalls

Rotated tick labels commonly clip at the figure edge. fig.tight_layout() or fig.subplots_adjust(bottom=...) gives the labels room:

fig.tight_layout()

If labels still clip, increase the bottom margin:

fig.subplots_adjust(bottom=0.2)

The rotation_mode parameter changes how rotation interacts with alignment. The default, rotation_mode='default', rotates the text around its anchor point, which can shift the label visually. rotation_mode='anchor' keeps the anchor point fixed:

ax.set_xticklabels(labels, rotation=45, ha='right', rotation_mode='anchor')

Use rotation_mode='anchor' when labels appear to drift away from their tick marks after rotation.

For long labels, consider wrapping with ax.set_xticklabels(labels, wrap=True) or reducing the number of ticks with ax.xaxis.set_major_locator(plt.MaxNLocator(6)).

Performance and Maintainability Considerations

Creating hundreds of text or annotate calls in a loop is acceptable for a few dozen elements, but for thousands of labels the rendering cost grows. Prefer ax.text with a single loop over many annotate calls when no arrow is needed, since annotate builds more internal objects.

For tick labels, avoid calling set_xticklabels repeatedly on the same axes. Each call rebuilds the label list. Set the labels once, then use tick_params for styling changes.

When the same rotation and annotation logic appears across multiple figures, wrap it in a helper function:

def style_axis(ax, rotation=45): ax.tick_params(axis='x', rotation=rotation) return ax

This keeps the styling consistent and reduces the chance of drift between figures.

python matplotlib ticks rotation text and annotations: Pract | RYUSLOG DEV