How to get final figure width (uselatex)

Viewed 97

I am writing a latex document and using matplotlib for plots. I want to have the font and font size (9) of the captions of my latex document also for the plot axes and legend text. Furthermore, I would like to fill out the \linewidth or \textwidth of my latex document, which is 369 pt. Now the matplotlib.pyplot.figure function accepts the input parameter figsize which should be in inches, so I duly specify it as 369/72 inches, 1/72 being the conversion factor from pt to inches. Later I cut down excess white space by using the bbox_inches=tight and pad_inches=0 options of the savefig function. The font and font size part works as intended. It looks exactly identical between the figure text and the caption text. However, I am still dissatisfied with the figure width. Below is a minimal example of a figure I produce.

import matplotlib
import matplotlib.pyplot as plt

plt.rcdefaults()

plt.rcParams['font.size'] = '9'
plt.rcParams['figure.autolayout'] = False
matplotlib.rc('font', family='sans-serif', serif=['Palatino'])
matplotlib.rc('text', usetex=True)
params = {'text.latex.preamble': [
    r'\usepackage[american]{babel}',
    r'\usepackage{mathpazo}',
    r'\usepackage{amsmath,amssymb,amsfonts,mathrsfs}',
    r'\usepackage{textcomp}',
    ]}
plt.rcParams.update(params)
plt.rcParams['mathtext.default'] = 'regular'
plt.rcParams['legend.handlelength'] = 1
delta_adjust = 0
plt.rcParams['figure.subplot.bottom'] = delta_adjust
plt.rcParams['figure.subplot.top'] = 1 - delta_adjust
plt.rcParams['figure.subplot.left'] = delta_adjust
plt.rcParams['figure.subplot.right'] = 1 - delta_adjust
plt.rcParams['figure.subplot.hspace'] = 0.55
plt.rcParams['figure.subplot.wspace'] = 0.55

default_figsize=(369/72, 369/72)

fig = plt.figure(figsize=default_figsize)
sp1 = plt.subplot(3,3,1)
sp2 = plt.subplot(3,3,2)
sp3 = plt.subplot(3,3,3)
for sp in sp1, sp2, sp3:
    sp.set_title('Title')
    sp.set_xlabel('Xlabel')
    sp.set_ylabel('Ylabel')
twin = sp3.twinx()
twin.set_ylabel('Ylabel')

fig.set_size_inches(default_figsize)
fig.savefig('./example.pdf', transparent=False, bbox_inches='tight', pad_inches=0, ending='.pdf')

This is the result of the above code. The figure has a width of 429.356 pt instead of the desired 369 pt. When I increase the delta_adjust parameter in the code, I get smaller pdf widths.

[philipp@desktop scripts]$ python minimal_example.py 
[philipp@desktop scripts]$ pdfinfo example.pdf 
Creator:        matplotlib 3.1.2, http://matplotlib.org
Producer:       matplotlib pdf backend 3.1.2
CreationDate:   Thu Jan 13 11:41:13 2022 CET
Tagged:         no
UserProperties: no
Suspects:       no
Form:           none
JavaScript:     no
Pages:          1
Encrypted:      no
Page size:      429.356 x 130.412 pts
Page rot:       0
File size:      102863 bytes
Optimized:      no
PDF version:    1.4

When I scale the figsize parameter of the python code from 369 pt to 369*369/429 pt, I end up with a 386 pt pdf. I do not want to use a trial and error strategy to find the correct parameter. As a last resort, I could write an iterative program as a savefig routine but I would prefer to avoid this. For reference, here is the output of the program converted to png: image

In summary, I am looking for help on how to set the figure width reliably.

I am on Ubuntu 20.04, python 3.8, matplotlib 3.1.2, and I use the TkAgg backend which is the default.

Any help is appreciated.

1 Answers

Only after posting this question did this website recommend me the following question: How to get figure size and fontsize right for PDFs exported from matplotlib?

It turns out that bbox_inches=tight messes with the figure size. I removed this option and set delta_adjust = 0.1 in the code above. Now the figure has the expected size of exactly 369x369 pt. Most of it is whitespace, which I can remove using the pdfcrop command line utility.

The current script looks like this.

import os
import matplotlib
import matplotlib.pyplot as plt

plt.rcdefaults()

plt.rcParams['font.size'] = '9'
plt.rcParams['figure.autolayout'] = False
matplotlib.rc('font', family='sans-serif', serif=['Palatino'])
matplotlib.rc('text', usetex=True)
params = {'text.latex.preamble': [
    r'\usepackage[american]{babel}',
    r'\usepackage{mathpazo}',
    r'\usepackage{amsmath,amssymb,amsfonts,mathrsfs}',
    r'\usepackage{textcomp}',
    ]}
plt.rcParams.update(params)
plt.rcParams['mathtext.default'] = 'regular'
plt.rcParams['legend.handlelength'] = 1
delta_adjust_h = 0.1
delta_adjust_v = 0.1
plt.rcParams['figure.subplot.bottom'] = delta_adjust_v
plt.rcParams['figure.subplot.top'] = 1 - delta_adjust_v
plt.rcParams['figure.subplot.left'] = delta_adjust_h
plt.rcParams['figure.subplot.right'] = 1 - delta_adjust_h
plt.rcParams['figure.subplot.hspace'] = 0.55
plt.rcParams['figure.subplot.wspace'] = 0.55

#factor = 369/429.356
factor = 1
default_figsize=(369/72*factor, 369/72*factor)

fig = plt.figure(figsize=default_figsize)
sp1 = plt.subplot(3,3,1)
sp2 = plt.subplot(3,3,2)
sp3 = plt.subplot(3,3,3)
for sp in sp1, sp2, sp3:
    sp.set_title('Title')
    sp.set_xlabel('Xlabel')
    sp.set_ylabel('Ylabel')
twin = sp3.twinx()
twin.set_ylabel('Ylabel')

fig.set_size_inches(default_figsize)
#fig.savefig('./example.pdf', transparent=False, ending='.pdf', pad_inches=0)
#        bbox_inches='tight', pad_inches=0, ending='.pdf')

fig.savefig('./example.pdf', transparent=True, pad_inches=0, ending='.pdf')
os.system('pdfcrop ./example.pdf ./example_cropped.pdf')

The output of pdfinfo is the following:

[philipp@desktop scripts]$ pdfinfo example_cropped.pdf 
Creator:        TeX
Producer:       pdfTeX-1.40.20
CreationDate:   Thu Jan 13 13:28:25 2022 CET
ModDate:        Thu Jan 13 13:28:25 2022 CET
Tagged:         no
UserProperties: no
Suspects:       no
Form:           none
JavaScript:     no
Pages:          1
Encrypted:      no
Page size:      364 x 112 pts
Page rot:       0
File size:      102853 bytes
Optimized:      no
PDF version:    1.4

So it is still not perfect, as the figure width is slightly too small due to the cropping. Also it is now not guaranteed that all the contents fit within the printed pdf, which previously was ensured by the bbox_inches option. Nevertheless, this is an improvement as now the plot sizes can no longer exceed the latex \linewidth.

I may update this answer if I find a better solution.

Edit: There is a feature under development at matplotlib which would solve the problem: https://matplotlib.org/stable/tutorials/intermediate/constrainedlayout_guide.html I tried it but it seems to work best when the subplots are all created at once, such as with plt.subplots. I will have to change all my scripts, because currently I add subplots one by one with plt.subplot. Moreover I need to set the vertical figure size explicitly instead of simply generating a square figure and cropping all the unused whitespace.

Related