I need to generate my matplotlib plots using lualatex
instead of pdflatex
. Among other things, I am using fontspec
to change the document fonts. Below I am using this as an example and set lmroman10-regular.otf
as the font. This creates a few issues. One is that the handles in the legends are not fully centered and are followed by some whitespace before the right border:
The python code generating the intermediate .pgf
file looks like this:
import matplotlib
import numpy
from matplotlib import pyplot
x = numpy.linspace(-1, 1)
y = x ** 2
matplotlib.rcParams["figure.figsize"] = (3, 2.5)
matplotlib.rcParams["font.family"] = "serif"
matplotlib.rcParams["font.size"] = 10
matplotlib.rcParams["legend.fontsize"] = 8
matplotlib.rcParams["pgf.texsystem"] = "lualatex"
PREAMBLE = r"""\usepackage{ifluatex}
\ifluatex
\usepackage{fontspec}
\setmainfont{lmroman10-regular.otf}
\fi
"""
matplotlib.rcParams["text.latex.preamble"] = PREAMBLE
matplotlib.rcParams["pgf.preamble"] = PREAMBLE
matplotlib.rcParams["text.usetex"] = True
pyplot.plot(x, y, label="this is the data")
pyplot.legend()
pyplot.xlabel("xlabel")
pyplot.tight_layout()
pyplot.savefig("lualatex_test.pgf")
The .pgf
file is then embedded in a latex document. It seems it is not possible to directly compile to .pdf
since then the document font will not be the selected font which can for example be seen by setting the font to someting more different like AntPoltExpd-Italic.otf
. Also note note that the \ifluatex
statement has to be added around the lualatex
-only code since matplotlib
uses pdflatex
to determine the dimensions of text fragements, as will be seen below.
For the sake of this simple example, the .tex
file to render the .pgf
may be just:
\documentclass[11pt]{scrbook}
\usepackage{pgf}
\usepackage{fontspec}
\setmainfont{lmroman10-regular.otf}
\newcommand{\mathdefault}[1]{#1}
\begin{document}
\input{lualatex_test.pgf}
\end{document}
which can be typeset using lualatex <filename>
and results in the figure shown above (without the red arrow).
I thought I had identified the reason for this but it seems I missed something. As mentioned above, matplotlib computes the dimensions of the text patches by actually placing them in a latex templated and compiling it using pdflatex
. This happens in the matplotlib.texmanager.TexManager
class in the corresponding file on the main branch for example. I thought I could fix it like this:
class TexManager:
...
@classmethod
def get_text_width_height_descent(cls, tex, fontsize, renderer=None):
"""Return width, height and descent of the text."""
if tex.strip() == '':
return 0, 0, 0
dvifile = cls.make_dvi(tex, fontsize)
dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:
page, = dvi
# A total height (including the descent) needs to be returned.
w = page.width
# !!!
if tex == "this is the data":
w /= 1.14
print("fixed width")
# !!!
return w, page.height + page.descent, page.descent
which, to my understanding, should trick matplotlib into thinking the text is shorter by a factor of 1.14 (just a guess, should be adapted once the solution works). The code definitely gets called, since "fixed width" gets printed. But the gap is not fixed:
How can I fix this issue? How is matplotlib computing the legend content's width and can I maybe patch this to account for the correct width? Let's assume I know that the width error factor for the font size 8 is approximately 1.14. I can easily determine this for other fonts and font sizes.
It seems that when using custom font settings, you have to set the following rcParam:
matplotlib.rcParams["pgf.rcfonts"] = False
Otherwise, if I understand it correctly, the font settings are applied from the rcParams.
This solves the spacing issue with your example:
For more details, see also the documentation of the .pgf
backend where this parameter is exaplained.