Custom ticks label in scientific notation while using log scale

Question:

I’m having trouble with matplotlib (version 3.1.3) : I would like to add custom ticks and tick labels on a log scale axis while preserving scientific notation.

To say it otherwise: I want to add custom ticks on a log scale axis and label them using the good old-fashioned ‘%1.1e’ (or whatever numbers) format but, for instance, instead of having ‘2.5e-02’, I would like to have ‘2.5 x 10^-2’ (or ‘2.5 times 10^{-2}’ in latex).

So I start with a minimum working piece of code without custom ticks:

import matplotlib as mpl
import matplotlib.pyplot as plt
print('MATPLOTLIB  VERSION : %s' % mpl.__version__)

plt.style.use("default")

# DATA
x = [0.1, 0.075, 0.05, 0.025, 0.01, 0.0075, 0.005, 0.0025, 0.001, 0.00075, 0.0005, 0.00025, 0.0001, 7.5e-05, 5e-05, 2.5e-05, 1e-05, 1e-06, 1e-07, 1e-08, 1e-09, 1e-10]
y = x

fig = plt.figure()
ax = plt.axes()
plt.loglog()
plt.minorticks_off()
path = ax.plot(x, y)

plt.savefig('test.png')

which gives:

enter image description here

Nice but, as I said, I would like to add custom ticks on the xaxis. More precisely, I would like to put limits to the axis and define equally log-spaced labels between those limits. Let’s say I want 4 ticks; it gives the following piece of code:

import matplotlib as mpl
import matplotlib.pyplot as plt
print('MATPLOTLIB  VERSION : %s' % mpl.__version__)

plt.style.use("default")

# DATA
x = [0.1, 0.075, 0.05, 0.025, 0.01, 0.0075, 0.005, 0.0025, 0.001, 0.00075, 0.0005, 0.00025, 0.0001, 7.5e-05, 5e-05, 2.5e-05, 1e-05, 1e-06, 1e-07, 1e-08, 1e-09, 1e-10]
y = x

xmin = min(x)
xmax = max(x)
ymin = min(y)
ymax = max(y)

# XTICKS
nbdiv = 4
xTicks = []
k = pow((xmin/xmax),1./(nbdiv-1.))
for i in range(0,nbdiv):
  xTicks.append(xmax*pow(k,i))

# PLOT
fig = plt.figure()
ax = plt.axes()
plt.loglog()
plt.minorticks_off()
plt.axis([xmin,xmax,ymin,ymax])
plt.xticks(xTicks)
path = ax.plot(x, y)

plt.savefig('test_working_4.png')

which provides the following plot:

enter image description here

That’s kind of the result I wanted to obtain. However, if the number of ticks (nbdiv) changes, for instance becomes equal to 5, I get:

enter image description here

And this time only the first and last ticks are labelled. It appears that only numbers which are equal (or at least close) to an integer power of ten (10^n) are labelled. I tried to change the default tick format with a matplot.ticker.ScalarFormatter but I didn’t manage to tune it to solve this problem. I also tried LogFormatterMathText and LogFormatterSciNotation, it wasn’t better.

The issue in itself doesn’t seem so difficult to me, so I don’t understand why I’m having so much trouble… What am I doing wrong? Does someone has keys to make me understand my errors?

In any case, I thank for reading and I thank you in advance for your response.

P.S.: Sorry for potential english mistakes, it’s not my native language.

Asked By: Vost

||

Answers:

Sorry for double posting but I came with a solution which, even if unsatisfactory, may be useful for others.

I find my solution unsatisfactory because it involves converting the format “by hand” with the following function of mine (I am no expert in Python so I am sure it could be simplified/optimized):

def convert_e_format_to_latex(numberAsStr):
# CONVERTS THE STRING numberAsStr IN FORMAT '%X.Ye' TO A LATEX 'X \times 10^{Y}'
  baseStr = list(numberAsStr)
  ind = 0
  i = 0
  flag = True
  nStr = len(baseStr)
  while (i < nStr and flag):
    if (baseStr[i] == 'e' or baseStr[i] == 'E'): # NOT USING FIND BECAUSE 'e' CAN ALSO BE CAPITAL
      ind = i
      flag = False
    i += 1
  if (flag):
    print('ERROR: badly formatted input number')
    return ''
  else:
    expo = str(int(''.join(baseStr[ind+1:nStr]))) # GET RID OF POTENTIAL ZEROS
    root = ''.join(baseStr[0:ind])
    indP = root.find('.')
    integerPart = int(root[0:indP]) #integer
    decimalPart = root[indP+1:ind] #string
    if (integerPart == 1): #DETECTING IF THE ROOT IS ONE (positive value)
      x = ''.center(ind-(indP+1),'0')
      if (decimalPart == x):
        return '$10^{'+expo+'}$'
      else:
        return '$'+str(integerPart)+'.'+decimalPart+' \times 10^{'+expo+'}$'
    elif (integerPart== -1): #DETECTING IF THE ROOT IS ONE (positive value)
      x = ''.center(ind-(indP+1),'0')
      if (decimalPart == x):
        return '$-10^{'+expo+'}$'
      else:
        return '$'+str(integerPart)+'.'+decimalPart+' \times 10^{'+expo+'}$'
    else:
      return '$'+str(integerPart)+'.'+decimalPart+' \times 10^{'+expo+'}$'

Then I add, in my previous piece of code:

for i in range(0,nbdiv):
  xTicksStr.append(convert_e_format_to_latex('%1.1e'%xTicks[i]))

And I change the xticks instruction in the plot which becomes:

plt.xticks(xTicks,xTicksStr)

This gives the wanted output:

enter image description here

It works, but it is so complicated… I am pretty sure I missed a simpler functionality. What do you think?

Answered By: Vost

Solved, based on your code above. This one is much simpler. You need to use xticklabels.

%matplotlib inline
import matplotlib as mpl
import matplotlib.pyplot as plt
from sympy import pretty_print as pp, latex
print('MATPLOTLIB  VERSION : %s' % mpl.__version__)

plt.style.use("default")

# DATA
x = [0.1, 0.075, 0.05, 0.025, 0.01, 0.0075, 0.005, 0.0025, 0.001, 0.00075, 0.0005, 0.00025, 0.0001, 7.5e-05, 5e-05, 2.5e-05, 1e-05, 1e-06, 1e-07, 1e-08, 1e-09, 1e-10]
y = x

xmin = min(x)
xmax = max(x)
ymin = min(y)
ymax = max(y)

# XTICKS
nbdiv = 5
xTicks = []
xticklabels = []
k = pow((xmin/xmax),1./(nbdiv-1.))
for i in range(0,nbdiv):
  xTicks.append(xmax*pow(k,i))
  printstr = '{:.2e}'.format(xmax*pow(k,i))
  ls = printstr.split('e')
  xticklabels.append((ls[0]+' x $10^{'  +ls[1] + '}$'))

# PLOT
fig = plt.figure()
ax = plt.axes()
plt.loglog()
plt.minorticks_off()
plt.axis([xmin,xmax,ymin,ymax])
plt.xticks(xTicks)
path = ax.plot(x, y)

plt.savefig('test_working_4.png')
ax.set_xticklabels(xticklabels)

enter image description here

Answered By: optimist