pythonmatplotlibunicodelatextext-rendering

Matplotlib mathtext: Glyph errors in tick labels


I've observed errors when rendering math in matplotlib 2.0.2, when using the default mathtext as opposed to the LaTeX math rendering engine. It seems that some glyphs (in my case the minus and the multiplication sign) is not recognized by mathtext. What makes it really weird is that the error only occurs when these particular glyphs appear in tick labels. When I deliberately type some mathy expression into e.g. the figure title, it works fine.

Consider the below example and the resultant image:

import matplotlib
import matplotlib.pyplot as plt

# 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',
                            })

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

test.png

As seen on the image, multiplication and some minus signs in the tick labels have been replaced with other characters. If I use LaTeX (by setting 'text.usetex' to True), everything renders nicely. Why does this happen, and more importantly, how can I fix it without changing from mathtext to LaTeX?

Additional information

This is the warning which gets printed when running the example code:

mathtext.py:866: MathTextWarning: Font 'default' does not have a glyph for '\times' [U+d7]
  MathTextWarning)
mathtext.py:867: MathTextWarning: Substituting with a dummy symbol.
  warn("Substituting with a dummy symbol.", MathTextWarning)

Note that the minus signs appearing in exponents get rendered correctly. These also do not render probably if I leave out 'mathtext.fontset': 'cm', producing another, similar warning:

mathtext.py:866: MathTextWarning: Font 'default' does not have a glyph for '-' [U+2212]
  MathTextWarning)
mathtext.py:867: MathTextWarning: Substituting with a dummy symbol.
  warn("Substituting with a dummy symbol.", MathTextWarning)

Also, if I include 'axes.unicode_minus': False in the rcParams (and keep 'mathtext.fontset': 'cm'), all minus signs render properly, though the problem remains for the multiplication signs.

The multiplication sign error do not seem to be a problem on older versions of matplotlib (I've tested 1.5.1, 1.4.3 and 1.3.1). However, these matplotib's insist on only producing tick labels at 10⁻², 10⁻¹, 1, 10, 10² etc., and so no multiplication sign is ever needed.

Bug report

This has been submitted as a bug report to Matplotlib.


Solution

  • 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.