2

I'm having trouble getting a custom font to work with Matplotlib (version 3.1.1) on Python 3.7.3 on Windows. The standard way using

import matplotlib as mpl
mpl.rcParams['font.family'] = 'sans-serif'
mpl.rcParams['font.sans-serif'] = [FONTNAME] 

works fine for a range of fonts preinstalled on the system. I recently manually installed the Lato family of fonts. However, when I use 'Lato' as FONTNAME, Matplotlib defaults back to Deja Vu Sans and doesn't even throw any errors. I also rebuilt the font cache using

mpl.font_manager._rebuild()

Several fonts named 'Lato' now appear when I run

mpl.font_manager.fontManager.ttflist

such as

 <Font 'Lato' (Lato-Semibold.ttf) normal normal semibold normal>,
 <Font 'Lato' (Lato-Thin.ttf) normal normal 400 normal>,
...

Yet the plots still look like they use Deja Vu Sans. I've looked all over but couldn't find a solution to this issue.

Vibex
  • 115
  • 1
  • 8

2 Answers2

8

The font properties in matplotlib's plot styles are managed by a FontManager class and are specified with a FontProperties class. To fetch these font properties, matplotlib internally uses an instance of the FontManager class to call a findfont() function that searches for fonts and returns the best TrueType (TTF) font file in the local or system font path that matches the font specifications in the FontProperties instance. The default fallback font in the specification is DejaVu Sans. The font family can be set to either of the following parameters: 'serif', 'sans-serif', 'cursive', 'fantasy', or 'monospace'. It is possible to find out the TTF file location for any of the font families like this:

In [1]: from matplotlib.font_manager import findfont, FontProperties

In [2]: font = findfont(FontProperties(family=['sans-serif']))

In [3]: font
Out[3]: 'C:\\Users\\xxxxxx\\Anaconda3\\envs\\py3.7.4\\lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\DejaVuSans.ttf'

Another example for the 'monospace' family:

In [7]: font = findfont(FontProperties(family=['monospace']))

In [8]: font
Out[8]: 'C:\\Users\\xxxxxx\\Anaconda3\\envs\\py3.7.4\\lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\DejaVuSansMono.ttf'

As you can see, the sans-serif family above is pointing to the default DejaVuSans TTF file because I haven't yet set the FONTNAME to something else like the 'Lato' font which also belongs to the sans-serif family.

Before, I change the FONTNAME, it is important to understand first how the font search happens. Now font search is an expensive task and to make this efficient for subsequent requests, the font info is cached in a JSON file. You can find the evidence in the source code for the FontManager class. For Windows, this file is found at: %userprofile%\.matplotlib. For more details, refer to the Notes section of the FontManager class docs:

This performs a nearest neighbor search. Each font is given a similarity score to the target font properties. The first font with the highest score is returned. If no matches below a certain threshold are found, the default font (usually DejaVu Sans) is returned.

The result is cached, so subsequent lookups don't have to perform the O(n) nearest neighbor search.

On my computer(Windows 10), I had two cache files: fontlist-v300 & fontlist-v310. If you examine the contents of any of these files, it shows a list of fonts and their properties such as TTF file location, style, weight etc. Observe the default family key:

"defaultFamily": {
    "ttf": "DejaVu Sans",
    "afm": "Helvetica"
  }

At this point, we understand that the font will display in DejaVu Sans. This is most noticeable in the title of the plot:

In [1]: import matplotlib as mpl
   ...: mpl.rcParams['font.family'] = 'sans-serif'
   ...: import matplotlib.pyplot as plt
   ...: plt.plot(range(0,50,10))
   ...: plt.title('Font test', size=32)
   ...: plt.show()

Plot(default font):

default font fallback

The findfont() function will always look for the cache file(and create one if it doesn't exist) and if I install a new font on my computer, it is important that this cache file is updated otherwise it would continue to display the fallback font(which is same as default). Before proceeding to next steps, make sure the Lato font is installed correctly. The font should be available under Fonts in the Control Panel.

Now with the Lato font properly installed, delete the cache file(s) and set the sans-serif font to Lato:

In [4]: import matplotlib as mpl^M
   ...: mpl.rcParams['font.family'] = 'sans-serif'
   ...: mpl.rcParams['font.sans-serif'] = 'Lato'
   ...: import matplotlib.pyplot as plt
   ...: plt.plot(range(0,50,10))
   ...: plt.title('Font test', size=32)
   ...: plt.show()

Plot(Lato sans-serif font):

enter image description here

You will also observe a new cache file has been created. The above snippet has re-build the cache file, which now also has Lato fonts' info. Again, you can open this cache file in a text editor to verify its presence. Let's verify the TTF file path for the sans-serif family now:

In [4]: from matplotlib.font_manager import findfont, FontProperties

In [5]: font = findfont(FontProperties(family=['sans-serif']))

In [6]: font
Out[6]: 'C:\\Users\\xxxxx\\AppData\\Local\\Microsoft\\Windows\\Fonts\\Lato-Thin.ttf'

As you can see that the sans-serif family is now pointing to the Lato-Thin TTF file.

Changing the font style to italic also requires that the cache file is deleted first:

In [3]: In [4]: import matplotlib as mpl
   ...:    ...: mpl.rcParams['font.family'] = 'sans-serif'
   ...:    ...: mpl.rcParams['font.sans-serif'] = 'Lato'
   ...:    ...: mpl.rcParams['font.style'] = 'italic'
   ...:    ...: import matplotlib.pyplot as plt
   ...:    ...: plt.plot(range(0,50,10))
   ...:    ...: plt.title('Font test', size=32)
   ...:    ...: plt.show()

In [4]: from matplotlib.font_manager import findfont, FontProperties

In [5]: font = findfont(FontProperties(family=['sans-serif']))

In [6]: font
Out[6]: 'C:\\Users\\xxxxxx\\AppData\\Local\\Microsoft\\Windows\\Fonts\\Lato-HairlineItalic.ttf'

Plot:

enter image description here

Note: All the steps were performed on IPython console, it may be required to restart the IPython session in order for the changes to take effect.

amanb
  • 5,276
  • 3
  • 19
  • 38
  • I'd mostly followed the above procedure when I posted the question, so I can only think of two possible culprits for my initial problem: I didn't restart my Spyder session; I had the Lato family installed locally in users\xxx\AppData\Local\Microsoft\Windows\Fontsrather than in windows\fonts. This seems to be the default Windows behaviour. – Vibex Jan 06 '20 at 08:23
2

If you want to use a newly-installed font, then instead of rebuilding the cache, you have to first delete and update the cache as @amanb suggested, then you have to restart the IPython session / Jupyter notebook / whatever you're programming Python on, before matplotlib.rcParams will recognize your new font.

The link I gave is instructions for Linux, but the idea is the same - delete the old cache, so that a new one will be created that includes your new font. For Windows, simply enter %userprofile%/.matplotlib into File Explorer and you will see a file like fontlist-v300.json. That's the font cache file. You can also run matplotlib.get_cachedir() in Python to see the path of the cache file.

You should delete that cache file. Then restart your session/notebook, and run

matplotlib.rcParams['font.family'] = 'sans-serif'
matplotlib.rcParams['font.sans-serif'] = [FONTNAME]

and see if your new font is displayed now. (A new cache will be created.)

As Lato is not newly-installed on this (Linux) PC, running the following code (which sets and finds the font) did give the correct fonts. So, I'm pretty sure the problem lies in your font cache not being updated after installing a new font.

import matplotlib as mpl
from matplotlib.font_manager import findfont, FontProperties
import matplotlib.pyplot as plt

x = [1, 2, 3]
y = [4, 5, 6]

mpl.rcParams['font.sans-serif'] = ['Lato']               # sets font to Lato
mpl.rcParams['font.weight'] = 'heavy'                    # otherwise can't see it lol
font = findfont(FontProperties(family=['sans-serif']))   # finds the font used
print(font)
plt.plot(x, y)
plt.title("Hello", size=45)
plt.show()

mpl.rcParams['font.sans-serif'] = ['DejaVu Sans']
mpl.rcParams['font.weight'] = 'normal'
font = findfont(FontProperties(family=['sans-serif']))
print(font)
plt.plot(x, y)
plt.title("Hello", size=45)
plt.show()

enter image description here

mehfluffy
  • 49
  • 1
  • 9