Montag, 3. März 2025

matplotlib - The secret of the vanishing x-ticks

The versions:
* Ubuntu 24.04
* python 3.12.3
* matplotlib 3.10.0

I've searched for this solution for days. So I describe it here for anyone who might need ist.

The goal is rather simple:

I want to create a figure with three subplots, each with an independent x-axis because I want to display data with different time periods.

I expected to get something like this:

Three separate subplots, each with its own labels on the x-axis showing a grid as well as date and time.

And that is exactly what you get if execute this simple program.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import matplotlib.dates as mdates
import matplotlib.pyplot as plt
from datetime import datetime

import matplotlib as mp
print(mp.__version__)
# 3.10.0

FORMAT_MAJOR = False
FORMAT_MINOR = False

# Format definitions
# not all are used

years = mdates.YearLocator()            # every year
months = mdates.MonthLocator()          # every month
days = mdates.DayLocator()              # every day
hours = mdates.HourLocator()            # every hour
years_fmt = mdates.DateFormatter('%Y')
month_fmt = mdates.DateFormatter('%m')
day_fmt = mdates.DateFormatter('%d')
hour_fmt = mdates.DateFormatter('%H')

fig, axs = plt.subplots(nrows=3, ncols=1, figsize=(187, 12), sharex="none")

datx0 = [ datetime(2025, 1, 31), datetime(2025, 2, 2), datetime(2025, 2, 3) ]
daty0 = [100, 200, 150]

datx1 = [ datetime(2025, 2, 4), datetime(2025, 2, 5), datetime(2025, 2, 7) ]
daty1 = [150, 100, 150]

datx2 = [ datetime(2025, 2, 1), datetime(2025, 2, 4), datetime(2025, 2, 5) ]
daty2 = [200, 200, 150]

axs[0].plot(datx0, daty0)
axs[1].plot(datx1, daty1)
axs[2].plot(datx2, daty2)

for pos in range(3):  # 0..2
    curraxs = axs[pos]
    curraxs.grid(True)

    if FORMAT_MAJOR:
        curraxs.xaxis.set_major_locator(days)
        curraxs.xaxis.set_major_formatter(day_fmt)
        curraxs.tick_params(axis="x", which="major", rotation=45)

    if FORMAT_MINOR:
        curraxs.xaxis.set_minor_locator(hours)
        curraxs.xaxis.set_minor_formatter(hour_fmt)
        curraxs.tick_params(axis="x", which="minor", rotation=90)

    # only 1% "slack" at each end
    curraxs.set_xmargin(0.01)

print(axs[0].xaxis.get_majorticklabels())
print(axs[1].xaxis.get_majorticklabels())
print(axs[2].xaxis.get_majorticklabels())

plt.show()


As you can see, there are three data series.

  • The first from 2025-1-31 to 2025-2-3.
  • The second from 2025-2-4 to 2025-2-7.
  • The third from 2025-2-1 to 2025-2-5.

The date ranges have been chosen to overlap slightly.  The y-data has no special meaning other than to show different graphs in the subplots.

The vanishing act occurs if you try to format the x-axis labels.

This is usually done with:


import matplotlib.dates as mdates

days = mdates.DayLocator()
day_fmt = mdates.DateFormatter('%d')

axs.xaxis.set_major_locator(days)
axs.xaxis.set_major_formatter(day_fmt)


This works fine for a single axis.  If you have more than one, strange things happen:

That’s the output with the variable FORMAT_MAJOR set to True.

The missing x-ticks become more apparent if you set FORMAT_MINOR to True as well.


  • In the first subplot the ticks for 2025-01-31 are missing.
  • In the second subplot the ticks from 2025-02-05 and above are missing.
  • Only the third subplot has all x-ticks.

The output of the get_majorticklabels() of the three axis at the end of the program...

print(axs[0].xaxis.get_majorticklabels())
print(axs[1].xaxis.get_majorticklabels())
print(axs[2].xaxis.get_majorticklabels())

...gives an indication of what happened:

They are all identical – using the values from the last call 2025-01-01 to 2025-02-05.
Which explains the missing parts at the beginning of the first subplot and the missing days at the end of the second.

So, how to fix this?

It seems that – contrary to what one might believe – the xxxxLocator() calls are not simply generators that produce ticks as requested.  They seem to keep some kind of internal state – in this case of the last subplot – influencing all the other uses.

You have to move them into the for-loop so that for each axis a “new” xxxxLocator() is created.

...

for pos in range(3):  # 0..2
    curraxs = axs[pos]
    curraxs.grid(True)

    days = mdates.DayLocator()
    hours = mdates.HourLocator()


    if FORMAT_MAJOR:
        curraxs.xaxis.set_major_locator(days)
        curraxs.xaxis.set_major_formatter(day_fmt)
        curraxs.tick_params(axis="x", which="major", rotation=45)
        
    ...


This gives the expected result:





Keine Kommentare: