Getting started with tick locator and formatter on dates and logs

Struggling to customize ticks and labels: I have been able to replace ticks and labels with manually selected and formatted lists, but I’d like an automated solution based on the built-in matplotlib handlers instead, say:

from matplotlib.ticker import LogLocator, FuncFormatter
from matplotlib.dates import MonthLocator, DateFormatter, AutoDateLocator, ConciseDateFormatter

Putting data together:

import matplotlib.pyplot as plt
import pandas as pd
from pandas import Timestamp
data_dict = {'Date': {292: Timestamp('2019-01-01 00:00:00'), 293: Timestamp('2019-04-01 00:00:00'), 294: Timestamp('2019-07-01 00:00:00'), 295: Timestamp('2019-10-01 00:00:00'), 296: Timestamp('2020-01-01 00:00:00'), 297: Timestamp('2020-04-01 00:00:00'), 298: Timestamp('2020-07-01 00:00:00'), 299: Timestamp('2020-10-01 00:00:00'), 300: Timestamp('2021-01-01 00:00:00')}, 'Gross Domestic Product': {292: 21001.591, 293: 21289.268, 294: 21505.012, 295: 21694.458, 296: 21481.367, 297: 19477.444, 298: 21138.574, 299: 21477.597, 300: 22038.226}}
df = pd.DataFrame(data_dict)

Basic plot:

f, ax = plt.subplots()
df.plot(ax=ax, x='Date', legend=False)
ax.set_yscale('log')
plt.show()

plot1

Objectives:

  • Horizontal axis: display Jan, Jul instead of Q1, Q3, and not display Q2 , Q4 at all.
  • Vertical axis: display 4 labels (instead of 6) like so: 1,900, 20,000, 21,000, 22,000.

Here are some of the things I’ve tried:

ax.xaxis.set_minor_locator(MonthLocator(1,7))
ax.xaxis.set_minor_formatter(DateFormatter('%m'))
ax.yaxis.set_major_locator(LogLocator(base=10.0, numticks=4))
ax.yaxis.set_major_formatter(FuncFormatter(lambda x, p: format(int(x), ',')))

I also tried ConciseDateFormatter.

1 Like

First off, if you want to use matplotlib locators with pandas, you need to use x_compat=True I. Your plot call.

1 Like

Thanks, let me give it another try!

following on with the same data, adding x_compat=True to the call, more details of attempts and results, none of which come close to intended result.

To repeat, my objective is:

  • x-axis: Print the years in the format 2020 and the months corresponding to the first and third quarters abbreviated to Jan and Jul

  • y-axis: Print the labels 19,000, 20,000, 21,000, 22,000

1. Experiment with dates : attempts to use MonthLocator(1,7)
fails because 7th month is missing

from matplotlib.dates import YearLocator, MonthLocator, DateFormatter
f, ax = plt.subplots()
df.plot(ax=ax, x='Date', x_compat=True)
ax.xaxis.set_major_locator(YearLocator(1))
ax.xaxis.set_major_formatter(DateFormatter('\n%Y'))
ax.xaxis.set_minor_locator(MonthLocator(1,7))      # <- July missing 
ax.xaxis.set_minor_formatter(DateFormatter('%b'))
plt.savefig("dates-1.png")
plt.show() 

dates-1

AS A NEW USER I AM ALLOWED ONLY ONE EMBEDDED IMAGE. UNFORTUNATELY I MUST REMOVE THE IMAGES BELOW. THEY MAY BE REPRODUCED WITH THE CODE.

2. Experiment with dates: attempts to use ‘bymonth’ inside MonthLocator
bymonth=6 should show every 6 months, fails because only June appears

from matplotlib.dates import YearLocator, MonthLocator, DateFormatter
f, ax = plt.subplots()
df.plot(ax=ax, x='Date', x_compat=True)
ax.xaxis.set_major_locator(YearLocator(1))
ax.xaxis.set_major_formatter(DateFormatter('\n%Y'))
ax.xaxis.set_minor_locator(MonthLocator(bymonth=6))
ax.xaxis.set_minor_formatter(DateFormatter('%b'))
plt.savefig("dates-2.png")
plt.show() 

3: Experiment with dates: attempt to use ‘interval’ inside MonthLocator
not expected to work, but almost works
fails because prints Mar and Sep instead of Jan and July

from matplotlib.dates import YearLocator, MonthLocator, DateFormatter
f, ax = plt.subplots()
df.plot(ax=ax, x='Date', x_compat=True)
ax.xaxis.set_major_locator(YearLocator(1))
ax.xaxis.set_major_formatter(DateFormatter('\n%Y'))
ax.xaxis.set_minor_locator(MonthLocator(interval=6))
ax.xaxis.set_minor_formatter(DateFormatter('%b'))
plt.savefig("dates-3.png")
plt.show() 

4: Experiment with dates: combining interval and bymonth
with interval=1, bymonth=2 gives February, bymonth=3 gives March,
maybe a combo of interval and bymonth? unfortunately, couldn’t find the right way…

from matplotlib.dates import YearLocator, MonthLocator, DateFormatter
f, ax = plt.subplots()
df.plot(ax=ax, x='Date', x_compat=True)
ax.xaxis.set_major_locator(YearLocator(1))
ax.xaxis.set_major_formatter(DateFormatter('\n%Y'))
ax.xaxis.set_minor_locator(MonthLocator(interval=6, bymonth=7))
ax.xaxis.set_minor_formatter(DateFormatter('%b'))
ax.xaxis.set_tick_params(rotation=0)  # default is to tilt the date
ax.xaxis.set_tick_params(length=0, width=0, which='minor')  # hide extra tick 
ax.xaxis.set_label_text('')
plt.savefig("dates-4.png")
plt.show() 

Somewhat less successful than my disastrous attempts to customize the dates: my attempt to customize the log axis.

Experiment with log scale

from matplotlib.ticker import LogLocator, FuncFormatter
f, ax = plt.subplots()
df.plot(ax=ax, x='Date', x_compat=True)
ax.set_yscale('log')
ax.yaxis.set_major_locator(LogLocator(base=10.0, numticks=2))  # <- not helpful
plt.ticklabel_format(axis='y', style='plain')  # <- doesn't do a thing
ax.get_yaxis().set_major_formatter(FuncFormatter(lambda x, pos: format(int(x), ',')))
#ax.yaxis.set_major_formatter(FuncFormatter(lambda x, pos: format(int(x), ',')))
# what's the difference between ax.get_yaxis().set_etc.  and ax.yaxis.set_etc. ?
ax.yaxis.set_label_text('log scale')
plt.savefig("log-1.png")
plt.show() 

I did try y_compat=True, but that didn’t help either.

Below a few more failed attempts. I am staring at the docs, but just can’t find the right way.

from matplotlib.ticker import ScalarFormatter, DateFormatter, StrMethodFormatter

f, ax = plt.subplots()
df.plot(ax=ax, x='Date', x_compat=True)
plt.yscale('log')
plt.ylim(19000, 22000)
plt.minorticks_off()
plt.yticks([19000,20000,210000,22000], ['19,000','20,000','21,000','22,000'])
plt.ylabel('log scale')
plt.xticks(rotation=0)
plt.savefig("dates-7.png")
plt.show()

or this:

from matplotlib.ticker import ScalarFormatter, DateFormatter, StrMethodFormatter

f, ax = plt.subplots()
df.plot(ax=ax, x='Date', y_compat=True)
plt.yscale('log')
# x-axis
plt.gca().xaxis.set_major_formatter(DateFormatter('\n%Y'))
plt.gca().xaxis.set_minor_locator(MonthLocator(1,7))
plt.gca().xaxis.set_minor_formatter(DateFormatter('%b'))
# y-axis
plt.gca().yaxis.set_major_formatter(ScalarFormatter())
plt.gca().yaxis.set_major_formatter(StrMethodFormatter("{x:g}"))
plt.ylabel('log scale')
plt.xticks(rotation=0)
plt.savefig("dates-8.png")
plt.show()

DESIRED LOOK FOR THE LOG SCALE:

MonthLocator(1,7) puts a tick on Jan 7 of each year. You want MonthLocator([1, 7])

1 Like

Thanks Jody! So I tried your suggestion, but couldn’t make it work. Now I think it may be because I’m trying to have the year as a major locator with the month as a minor locator and January appears twice, so it’s overwritten — or something. After a few more hours of matplotlib practice I’ll probably understand what you were trying to say, but right now I’m still stumped. However, I did find a way. Below is finally a solution for the x-axis. I’m now going to focus my energies trying to understand the y-axis!

In words: For each year, use the YearLocator(1) to add Jan 2019, Jan 2020, etc… Then, use the MonthLocator(7) to add Jul for every year.

Verdict: x-axis challenge solved!

f, ax = plt.subplots()
df.plot(ax=ax, x='Date', x_compat=True)
ax.xaxis.set_major_locator(YearLocator(1))
ax.xaxis.set_major_formatter(DateFormatter("%b\n%Y"))
ax.xaxis.set_tick_params(rotation=0)
ax.xaxis.set_minor_locator(MonthLocator(7))
ax.xaxis.set_minor_formatter(DateFormatter("%b"))
plt.show() 

dates-5

Have you tried just using the ConciseFormatter. It doesn’t write out “Jan” but that is kind of implied by having the year on the year tick.

I have tried this:

ax.xaxis.set_major_formatter(ConciseDateFormatter("%b\n%Y"))

which gives a layout similar to the desired layout, with months on the first row and year on the second row, but I couldn’t control the frequency and, as you say, it will print the year where January belongs. It’s definitely less work and a nice default. :+1:

I’ve made progress with the log scale too, but I haven’t figured out how to set ticks at multiples of 10,000.

It turns out that one of the hurdles I was facing was that I assumed the log ticks were major ticks, but as it turns out they are minor. Why are the ticks on the log scale minor ticks?

I use ScalarFormatter together with FuncFormatter to get the formatting to my taste. Next (and last) step is to figure out how to have ticks at multiples of 10,000.

from matplotlib.ticker import LogLocator, FuncFormatter, NullLocator, ScalarFormatter

f, ax = plt.subplots()
df.plot(ax=ax, x='Date', x_compat=True)
# x-axis (don't let dates distract us)
ax.xaxis.set_major_locator(NullLocator())
ax.xaxis.set_label_text('')
# y-axis (set log scale and nice labels)
ax.set_yscale('log') # set log scale
plt.ylim(18500, 22500) # enlarge limits
# the y ticks are "minor" ticks! why?
ax.yaxis.set_minor_formatter(ScalarFormatter())
ax.yaxis.set_minor_formatter(FuncFormatter(lambda x, pos: format(int(x), ',')))
ax.yaxis.set_label_text('log scale')
plt.show() 

log-2

To control the frequency of ticks, I tried variations on the following, without any luck:

ax.xaxis.set_major_locator(LogLocator(base=10.0, numticks=4))
ax.xaxis.set_major_locator(LogLocator(base=10.0, subs=(0.1,1.0, )))

If I enlarge the limits to this:

plt.ylim(18500, 23200) # enlarge limits

I ‘miraculously’ obtain the desired multiples of 10,000.

log-2

How could I obtain these ticks without having to fiddle with the plot’s height?