The font I want to use doesn't have all the symbols I need. Is it possible to have matplotlib use a different font if a symbol is missing?
Here is a minimal example:
import matplotlib.pyplot as plt
fig = plt.figure()
plt.axis([0, 8, 0, 6])
t = u'abcde♥'
plt.text(4.5, 4, 'DejaVu Sans:', horizontalalignment='right')
plt.text(5, 4, t, {'family':'DejaVu Sans'})
plt.text(4.5, 3, 'Noto Sans:', horizontalalignment='right')
plt.text(5, 3, t, {'family':'Noto Sans'})
plt.text(4.5, 2, 'Noto Sans Symbols2:', horizontalalignment='right')
plt.text(5, 2, t, {'family':'Noto Sans Symbols2'})
plt.show()
And the output:
Noto Sans is missing the heart symbol while Noto Sans Symbols2 is missing the letters. I'm trying to get something like the DejaVu Sans example but with letters from Noto Sans and the heart from Noto Sans Symbols2.
Usually, double-click on the . ttf file and then click on the Install button in the window that pops up. Note that Matplotlib handles fonts in True Type Format (. ttf) , so make sure you install fonts ending in .
To map font names to font files, matplotlib has a dictionary (or json file) located in its cache directory. Note, this file is not always in the same place, but usually sits at the home directory. If you are on mac (windows), it usually sits at whereever your HOME (%HOME%) environmental variable is set to.
Matplotlib can use font families installed on the user's computer, i.e. Helvetica, Times, etc. Font families can also be specified with generic-family aliases like ( {'cursive', 'fantasy', 'monospace', 'sans', 'sans serif', 'sans-serif', 'serif'} ).
In Matplotlib, to set the title of a plot you have to use the title() method and pass the fontsize argument to change its font size.
Here's my thinking:
Create a function taking x
- the starting x
position, y
- the y
position, text
- the text to draw, and fallbackList
- a list of fonts, ordered like font-family
in CSS.
fontTools.ttLib.TTFont
to check if a certain character is contained within the primary font (fallbackList[0]
), by parsing the font table, looping through it, and checking if the given character is inside the map (see this question).False
(i.e it's not contained within the font pack), go through fallbackList
, repeating step 1, until you find a font that does contain it. If you find a character with no font containing it, just use the first font. We'll call this font we just found foundFont
.textpath.TextPath((xPosNow, y) ...stuff... prop=foundFont).getextents()
(matplotlib > 1.0.0). This draws that character, but also gets the bounding box of the text, from which you can extract the width (see this question). Do xPosNow += textWidth
, where textWidth
is extracted from getextents()
.This would essentially keep a tally of the total distance from the origin (by adding together the width of each bit of text you add), and then when you need to add another bit of text in a different font, simply set the x value to be this tally + a little bit for kerning, and this way, you can just work out where you want each character to go (but do each character separately).
Here's a code example:
import matplotlib.pyplot as plt
from matplotlib.textpath import TextPath
from fontTools.ttLib import TTFont
fig = plt.figure()
plt.axis([0, 8, 0, 6])
t = u'abcde♥'
plt.text(4.5, 4, 'DejaVu Sans:', horizontalalignment='right')
plt.text(5, 4, t, {'family':'DejaVu Sans'})
plt.text(4.5, 3, 'Noto Sans:', horizontalalignment='right')
plt.text(5, 3, t, {'family':'Noto Sans'})
plt.text(4.5, 2, 'Noto Sans Symbols2:', horizontalalignment='right')
plt.text(5, 2, t, {'family':'Noto Sans Symbols2'})
def doesContain(fontPath, unicode_char): # Helper function, the only issue being it takes font paths instead of names
font = TTFont(fontPath) # Use helper library to go through all characters
for cmap in font['cmap'].tables:
if cmap.isUnicode():
if ord(unicode_char) in cmap.cmap: # If the character table contains our character return True
return True
# Otherwise, return False.
return False
def renderText(x, y, text, fontSize, fallback_list, spacingSize):
xPosNow = x
for char in text: # For each character...
fontId = 0
while not doesContain(fallback_list[fontId]['path'], char): # find a font that works
if fontId < len(fallback_list) - 1:
fontId += 1
else: # Or just go with the first font, if nothing seems to match
fontId = 0
break
print(fontId)
t = plt.text(xPosNow, y, char, {'family':fallback_list[fontId]['name']})
r = fig.canvas.get_renderer()
xPosNow += t.get_window_extent(renderer=r).width/100 + spacingSize
We call it with:
renderText(3, 5, t, 9, [
{
'path': 'C:\\Users\\User\\Downloads\\NotoSans-hinted\\NotoSans-Regular.ttf', # Font path
'name': 'Noto Sans' # Font name
},
{
'path': 'C:\\Users\\User\\Downloads\\NotoSansSymbols2-unhinted\\NotoSansSymbols2-Regular.ttf',
'name': 'Noto Sans Symbols2'
}
]
, 0.08) # The distance between the letters
plt.show()
And the output is:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With