Matplotlib 2 mathtext: Glyph errors in tick labels

I find the STIX fonts to be acceptable substitutes for computer modern.

import matplotlib
import matplotlib.pyplot as plt

# Customize matplotlib
matplotlib.rcParams.update(
    {
        'text.usetex': False,
        'font.family': 'stixgeneral',
        'mathtext.fontset': 'stix',
    }
)

# Plot
plt.semilogy([-0.03, 0.05], [0.3, 0.05])
plt.title(r'$-6\times 10^{-2}$')
plt.savefig('test.png')

This produces the following output on my laptop:

enter image description here

Cause of problem

I now understand what is going on. The yticklabels all have a format similar to

r'$\mathdefault{6\times10^{-2}}$'

which works fine for major tick labels, where the \times10^{-2} part is absent. I believe this fails for minor tick labels because \times does not work inside of \mathdefault{}. As stated here, \mathdefault{} is used to produce regular (non-math) text with the same font as is used for mathtext, with the limitation that far fewer symbols are available. As everything inside of \mathdefault{} is math, the use of \mathdefault{} is completely redundant, and so it can safely be removed. This solves the issue.

Solution

One could solve this using matplotlib's tick formatters. I would like however to keep the default (minor) tick label positions and (intended) formatting, and so an easier solution is simply to rip out the \mathdefault part of the tick labels:

import warnings
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.mathtext import MathTextWarning

# Customize matplotlib
matplotlib.rcParams.update({# Use mathtext, not LaTeX
                            'text.usetex': False,
                            # Use the Computer modern font
                            'font.family': 'serif',
                            'font.serif': 'cmr10',
                            'mathtext.fontset': 'cm',
                            # Use ASCII minus
                            'axes.unicode_minus': False,
                            })
# Function implementing the fix
def fix(ax=None):
    if ax is None:
        ax = plt.gca()
    fig = ax.get_figure()
    # Force the figure to be drawn
    with warnings.catch_warnings():
        warnings.simplefilter('ignore', category=MathTextWarning)
        fig.canvas.draw()
    # Remove '\mathdefault' from all minor tick labels
    labels = [label.get_text().replace('\mathdefault', '')
              for label in ax.get_xminorticklabels()]
    ax.set_xticklabels(labels, minor=True)
    labels = [label.get_text().replace('\mathdefault', '')
              for label in ax.get_yminorticklabels()]
    ax.set_yticklabels(labels, minor=True)
# Plot
plt.semilogy([-0.03, 0.05], [0.3, 0.05])
plt.title(r'$-6\times 10^{-2}$')
fix()
plt.savefig('test.png')

The tricky part in writing this fix is the fact that you cannot get the tick labels before the figure has been drawn. Thus we need to first call fig.canvas.draw(). This will raise the warning, which I have suppressed. This also means that you should call fix() as late as possible, so that all axes gets drawn as they would in the end. Finally (as stated already in the question), the 'axes.unicode_minus' has been set to False to fix the similar issue with the minus signs.

The resultant image: test.png The keen LaTeX eye might spot that something is still slightly off regarding the minuses in the xticklabels. This is unrelated to the question, but happens because the numbers in the xticklabels are not enclosed in $...$.

Update for matplotlib 3.1.0

From matplotlib version 3.1.0, the warning is emitted through the logging module, not warnings. To silent the warning, replace

    # Force the figure to be drawn
    with warnings.catch_warnings():
        warnings.simplefilter('ignore', category=MathTextWarning)
        fig.canvas.draw()

with

    # Force the figure to be drawn
    import logging
    logger = logging.getLogger('matplotlib.mathtext')
    original_level = logger.getEffectiveLevel()
    logger.setLevel(logging.ERROR)
    with warnings.catch_warnings():
        warnings.simplefilter('ignore', category=MathTextWarning)
        fig.canvas.draw()
    logger.setLevel(original_level)

which now ignores the warning regardless of whether it is emitted through logging or warnings.