divenex
divenex

Reputation: 17198

Proper automatic logarithmic axes when plot range less than a factor 10 (Matplotlib)

When the data range of logarithmic plots does not include a full factor of 10, the current (v3.5) Matplotlib code does not automatically produce publication-quality figures (see below). How can one correct this limitation?

Here is an example of a minimal code to produce a figure with a logarithmic axis and a range that does not include a full factor of 10 from the plotted major tick mark.

import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0.2, 8, 100)
plt.semilogx(x, np.sin(x))

The resulting figure below shows a single tick label and is hard to read and not publication quality

enter image description here

Using the suggestions from this related question, I could plot all minor tick marks and switch to non-scientific notation as follows

import matplotlib.pyplot as plt
from matplotlib import ticker
import numpy as np

fig, ax = plt.subplots()
x = np.linspace(0.2, 8, 100)
plt.semilogx(x, np.sin(x))

formatter = ticker.FuncFormatter(lambda y, _: f'{y:.3g}')
ax.xaxis.set_major_formatter(formatter)
ax.xaxis.set_minor_formatter(formatter)

This generates the following figure, which has too dense minor tick marks labels and is also not publication-quality

enter image description here

I am aware that I could edit the tick labels by hand, but I am looking for a way to produce proper tick labels automatically for many figures, with different axis ranges.

The tick marks labels I am looking for are like the ones that Mathematica nicely generates in the same situation as follows. Is it possible to achieve something similar in Matplotlib?

enter image description here

To clarify my question, as a practical test, could one design a function that automatically produces proper tick labels when running my above procedure with either of the following three vectors x:

x = np.linspace(0.2, 8, 100)
x = np.linspace(3, 60, 100)
x = np.linspace(0.06, 0.5, 100)

NB: editing the tick labels by typing different values for each different case is not a useful solution, as my function has to run in a general procedure to be used with unknown x range.

Upvotes: 0

Views: 371

Answers (1)

Ka Wa Yip
Ka Wa Yip

Reputation: 2993

To reproduce something close to the mathematica's plot, you need to turn off the labeling of the minor ticks, i.e. the parts that are messed up.

Then you want to have a custom labeling of the major ticks. This can be done using FixedLocator.

import matplotlib.pyplot as plt
from matplotlib import ticker

import numpy as np

fig, ax = plt.subplots()
x = np.linspace(0.2, 8, 100)
plt.semilogx(x, np.sin(x))

formatter = ticker.FuncFormatter(lambda y, _: f'{y:.3g}')
ax.xaxis.set_major_formatter(formatter)
#ax.xaxis.set_minor_formatter(formatter) #comment this line out
ax.xaxis.set_major_locator(ticker.FixedLocator([0.2, 0.5, 1, 2, 5])) 

Output:

enter image description here

** First update:

For automatic spacing, I would recommend just employing the matplotlib original major_formatter that gives x-label only at points of which the log10 is an integer, i.e.:

import matplotlib.pyplot as plt
from matplotlib import ticker

import numpy as np

fig, ax = plt.subplots()
x = np.linspace(0.2, 8, 100)
plt.semilogx(x, np.sin(x))

formatter = ticker.FuncFormatter(lambda y, _: f'{y:.3g}')
ax.xaxis.set_major_formatter(formatter)

This seems to give ideal labeling spacings for every possible range of x for plotting purpose, irrespective of figure size, number of zeros, etc.

For your case of x = np.linspace(0.2, 8, 100), it gives enter image description here

On the other extreme, if x = np.linspace(0.000002, 800, 100): enter image description here

After such plot generation, one can always use FixedLocator if more xlabels (like 0,2, 0.5, 2, 5, 20, 50, ..) are needed/modified.

** Second update:

For the three cases that only span two orders of magnitude, one can automatically output the xticks (major ticks/the xlabels locations) with a xticksfinder function, without worrying too much about the number of zeros and figure size (which will lead to xlabels overlapping). As a general rule of thumb, one would want to specify {1, 2, 5} times 10^n, where n is the order of magnitude. Then the following xticksfinder function would be helpful

def order(num):
    return math.floor(math.log(num, 10))

def xticksfinder(x):
    x = x.tolist()
    xtickers = []
    for i in range(order(x[0]), order(x[-1])+1):
         xtickers.append(1*math.pow(10, i))
         xtickers.append(2*math.pow(10, i))
         xtickers.append(5*math.pow(10, i))
    xticks = [x[0]] + [item for item in xtickers if x[0] < item <= x[-1]]
    xticks = [int(k) if k >=1 else k for k in xticks]
    return xticks

Then you can call it in FixedLocator:

import matplotlib.pyplot as plt
from matplotlib import ticker

import numpy as np

fig, ax = plt.subplots()
x = np.linspace(0.2, 8, 100)
plt.semilogx(x, np.sin(x))

formatter = ticker.FuncFormatter(lambda y, _: f'{y:.3g}')
ax.xaxis.set_major_formatter(formatter)
#ax.xaxis.set_minor_formatter(formatter) #comment this line out
ax.xaxis.set_major_locator(ticker.FixedLocator(xticksfinder(x)))

For x = np.linspace(0.2, 8, 100), the output is: enter image description here

For x = np.linspace(3, 60, 100), the output is: enter image description here

For x = np.linspace(0.06, 0.5, 100), the output is: enter image description here

Upvotes: 3

Related Questions