2

How can I tell matplotlib to use a specific variant of a font when they both have the same name and characteristics?

For example, I can tell matplotlib to use Latin Modern Roman:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import numpy as np
from pathlib import Path

import matplotlib as mpl
import PyQt5
mpl.use('Qt5Agg')
import matplotlib.pyplot as plt

mpl.rcParams['font.family'] = 'Latin Modern Roman'
mpl.rcParams['font.weight'] = '400'
mpl.rcParams['font.style'] = 'normal'
mpl.rcParams['font.variant'] = 'normal'
mpl.rcParams['mathtext.fontset'] = 'custom'
mpl.rcParams['mathtext.default'] = 'it'
mpl.rcParams['mathtext.rm'] = 'Latin Modern Roman:normal'
mpl.rcParams['mathtext.it'] = 'Latin Modern Roman:italic'
mpl.rcParams['mathtext.bf'] = 'Latin Modern Roman:bold'

mpl.rcParams['xtick.labelsize'] = 8
mpl.rcParams['ytick.labelsize'] = 8
mpl.rcParams['axes.titlesize'] = 10
mpl.rcParams['axes.labelsize'] = 10

x = np.linspace(0,2*np.pi,100)
y = np.sin(x)

fig1 = plt.figure(figsize=(3, 3*(9/16)), dpi=300)
ax1 = fig1.gca()
ax1.plot(x, y, c='r', linewidth=1.0, zorder=20)
ax1.set_xlabel(r'$x\ label$')
ax1.set_ylabel(r'$y\ label$')
fig1.tight_layout(pad=0.15)
plt.show()

enter image description here

OK, that's nice. But what if I want to use a specific "substyle" of Latin Modern Roman? For example, on my system, when I list the available font entries having name='Latin Modern Roman' and weight=400 and style='normal' and variant='normal' through:

fonts = mpl.font_manager.fontManager.ttflist
for f in fonts:
    if (f.name=='Latin Modern Roman'):
        if all([(f.style=='normal'),(f.weight==400),(f.style=='normal'),(f.variant=='normal')]):
            print(f.name)
            print(Path(f.fname).stem)
            print(f.fname)
            print('weight : %s'%str(f.weight))
            print('style : %s'%str(f.style))
            print('stretch : %s'%str(f.stretch))
            print('variant : %s'%str(f.variant))
            print('\n')

I get:

Latin Modern Roman
lmroman9-regular
/usr/share/texmf/fonts/opentype/public/lm/lmroman9-regular.otf
weight : 400
style : normal
stretch : normal
variant : normal

...
...

Latin Modern Roman
lmroman10-regular
/usr/share/texmf/fonts/opentype/public/lm/lmroman10-regular.otf
weight : 400
style : normal
stretch : normal
variant : normal

So how would I tell matplotlib that I want to use lmroman9-regular.otf vs lmroman10-regular.otf? In mpl.rcParams I can only specify font.family,font.weight,font.style and font.variant so if the two .otf files have all the same values, how can I tell it to use one .otf rather than the other? There are actually differences between the fonts, for example:

enter image description here

I've tried referencing the .otf file directly with a matplotlib.FontProperties instance, then renaming it, then pointing to the new name like:

prop = mpl.font_manager.FontProperties(fname='/usr/share/fonts/opentype/lmodern/lmroman10-regular.otf')
prop.set_name('Latin Modern Roman Type 10')
mpl.rcParams['font.family'] = 'Latin Modern Roman Type 10'

but then I get findfont: Font family ['Latin Modern Roman Type 10'] not found. Falling back to DejaVu Sans. because prop.get_name() still (mysteriously) returns Latin Modern Roman and not Latin Modern Roman Type 10

How can I tell matplotlib that I want to use a one over the other?

It's possible to explicitly use fontproperties to point to a font_manager.FontProperties() instance in most set_label and set_ticklabel calls (like in this answer), but I'm looking for a way to set this globally through rcParams.

Note: I would NOT like to use LaTeX rendering (text.usetex) in matplotlib.

System:

  • Ubuntu-20.04 on WSL2 on Windows 10 Pro 19042
  • Python 3.8.5 / [GCC 9.3.0] on linux
  • matplotlib 3.4.1
HotDogCannon
  • 2,113
  • 11
  • 34
  • 53
  • have you tried setting `fontproperties=prop` as suggested in [this answer](https://stackoverflow.com/a/7728665/15744300)? – Jan Wilamowski May 13 '21 at 02:21
  • that doesn't work in math mode and also requires setting `fontproperties=prop` in every `set_label()` and `set_ticklabels()` call, which not convenient / maintainable. I'm looking for some way to set this globally through `rcParams` – HotDogCannon May 13 '21 at 13:23

1 Answers1

3

That is a really good question. I will provide the way I found the solution to that specific problem. In order to reproduce it we first have to download the given fonts from here (I downloaded the version 10 regular and the 17 version regular to see a clearer difference). Once installed we can check if our new font is found by matplotlib:

from matplotlib import font_manager

font_manager.font_manager.findSystemFonts(fontext='otf') # Matplotlib associates with
# the .otf extension two more extensions viz. '.ttc' and '.ttf', as can be obtain over
# font_manager.get_fontext_synonyms('otf')

Once you have found your specified path to your .otf file, we can use the font_manager to make that specific version our selected font.

# This will create an object which contains your file and give it a custom name
# Here is the idea of this: https://stackoverflow.com/questions/35668219/how-to-set-up-a-custom-font-with-custom-path-to-matplotlib-global-font
fe = font_manager.FontEntry(
    fname='C:\\Users\\<your_user_name>\\AppData\\Local\\Microsoft\\Windows\\Fonts\\latin-modern-roman.mroman10-regular.otf',
    name='10erLatin')
font_manager.fontManager.ttflist.insert(0, fe) 
mpl.rcParams['font.family'] = fe.name

Lets plot it so that we can later see whether it worked or not.

import numpy as np
from pathlib import Path

import matplotlib as mpl
# import PyQt5
# mpl.use('Qt5Agg') # I want to plot it inline ;)
import matplotlib.pyplot as plt

mpl.rcParams['font.family'] = fe.name
mpl.rcParams['font.weight'] = '400'
mpl.rcParams['font.style'] = 'normal'
mpl.rcParams['font.variant'] = 'normal'
mpl.rcParams['mathtext.fontset'] = 'custom'
mpl.rcParams['mathtext.default'] = 'it'
mpl.rcParams['mathtext.rm'] = fe.name + ':normal'
mpl.rcParams['mathtext.it'] = fe.name + ':italic'
mpl.rcParams['mathtext.bf'] = fe.name + ':bold'

mpl.rcParams['xtick.labelsize'] = 8
mpl.rcParams['ytick.labelsize'] = 8
mpl.rcParams['axes.titlesize'] = 10
mpl.rcParams['axes.labelsize'] = 10

x = np.linspace(0,2*np.pi,100)
y = np.sin(x)

fig1 = plt.figure(figsize=(3, 3*(9/16)), dpi=300)
ax1 = fig1.gca()
ax1.plot(x, y, c='r', linewidth=1.0, zorder=20)
ax1.set_xlabel(r'$x\ label$')
ax1.set_ylabel(r'$y\ label$')
fig1.tight_layout(pad=0.15)
plt.show()

enter image description here

Lets change it up to the 17 version of the regular font.

fe = font_manager.FontEntry(
    fname='C:\\Users\\<your_user_name>\\AppData\\Local\\Microsoft\\Windows\\Fonts\\latin-modern-roman.mroman17-regular.otf',
    name='17erLatin')
font_manager.fontManager.ttflist.insert(0, fe) 
mpl.rcParams['font.family'] = fe.name

Again, lets plot it (used the exact same code as above to create that plot, just executed the given code beforehand).

enter image description here

If my hindsight is not too bad, then the text changed globally. There are some other functions which might be useful to look at, however, they are really hidden in the source code. Please let me know, if you run into problems.

Yannik Suhre
  • 724
  • 5
  • 21