Reputation: 17198
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
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
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
?
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
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:
** 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
On the other extreme, if x = np.linspace(0.000002, 800, 100)
:
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:
For x = np.linspace(3, 60, 100)
, the output is:
For x = np.linspace(0.06, 0.5, 100)
, the output is:
Upvotes: 3