Comparison of different approaches
Here is a quick comparison of some of the approaches I've tried with images showing what the give.
Baseline example without trying to set the image dimensions
Just to have a comparison point:
base.py
#!/usr/bin/env python3
import sys
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
fig, ax = plt.subplots()
print('fig.dpi = {}'.format(fig.dpi))
print('fig.get_size_inches() = ' + str(fig.get_size_inches())
t = np.arange(-10., 10., 1.)
plt.plot(t, t, '.')
plt.plot(t, t**2, '.')
ax.text(0., 60., 'Hello', fontdict=dict(size=25))
plt.savefig('base.png', format='png')
run:
./base.py
identify base.png
outputs:
fig.dpi = 100.0
fig.get_size_inches() = [6.4 4.8]
base.png PNG 640x480 640x480+0+0 8-bit sRGB 13064B 0.000u 0:00.000
My best approach so far: plt.savefig(dpi=h/fig.get_size_inches()[1]
height-only control
I think this is what I'll go with most of the time, as it is simple and scales:
get_size.py
#!/usr/bin/env python3
import sys
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
height = int(sys.argv[1])
fig, ax = plt.subplots()
t = np.arange(-10., 10., 1.)
plt.plot(t, t, '.')
plt.plot(t, t**2, '.')
ax.text(0., 60., 'Hello', fontdict=dict(size=25))
plt.savefig(
'get_size.png',
format='png',
dpi=height/fig.get_size_inches()[1]
)
run:
./get_size.py 431
outputs:
get_size.png PNG 574x431 574x431+0+0 8-bit sRGB 10058B 0.000u 0:00.000
and
./get_size.py 1293
outputs:
main.png PNG 1724x1293 1724x1293+0+0 8-bit sRGB 46709B 0.000u 0:00.000
I tend to set just the height because I'm usually most concerned about how much vertical space the image is going to take up in the middle of my text.
plt.savefig(bbox_inches='tight'
changes image size
I always feel that there is too much white space around images, and tended to add bbox_inches='tight'
from:
Removing white space around a saved image in matplotlib
However, that works by cropping the image, and you won't get the desired sizes with it.
Instead, this other approach proposed in the same question seems to work well:
plt.tight_layout(pad=1)
plt.savefig(...
which gives the exact desired height for height equals 431:
Fixed height, set_aspect
, automatically sized width and small margins
Ermmm, set_aspect
messes things up again and prevents plt.tight_layout
from actually removing the margins...
plt.savefig(dpi=h/fig.get_size_inches()[1]
+ width control
If you really need a specific width in addition to height, this seems to work OK:
width.py
#!/usr/bin/env python3
import sys
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
h = int(sys.argv[1])
w = int(sys.argv[2])
fig, ax = plt.subplots()
wi, hi = fig.get_size_inches()
fig.set_size_inches(hi*(w/h), hi)
t = np.arange(-10., 10., 1.)
plt.plot(t, t, '.')
plt.plot(t, t**2, '.')
ax.text(0., 60., 'Hello', fontdict=dict(size=25))
plt.savefig(
'width.png',
format='png',
dpi=h/hi
)
run:
./width.py 431 869
output:
width.png PNG 869x431 869x431+0+0 8-bit sRGB 10965B 0.000u 0:00.000
and for a small width:
./width.py 431 869
output:
width.png PNG 211x431 211x431+0+0 8-bit sRGB 6949B 0.000u 0:00.000
So it does seem that fonts are scaling correctly, we just get some trouble for very small widths with labels getting cut off, e.g. the 100
on the top left.
I managed to work around those with Removing white space around a saved image in matplotlib
plt.tight_layout(pad=1)
which gives:
width.png PNG 211x431 211x431+0+0 8-bit sRGB 7134B 0.000u 0:00.000
From this, we also see that tight_layout
removes a lot of the empty space at the top of the image, so I just generally always use it.
Fixed magic base height, dpi
on fig.set_size_inches
and plt.savefig(dpi=
scaling
I believe that this is equivalent to the approach mentioned at: https://stackoverflow.com/a/13714720/895245
magic.py
#!/usr/bin/env python3
import sys
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
magic_height = 300
w = int(sys.argv[1])
h = int(sys.argv[2])
dpi = 80
fig, ax = plt.subplots(dpi=dpi)
fig.set_size_inches(magic_height*w/(h*dpi), magic_height/dpi)
t = np.arange(-10., 10., 1.)
plt.plot(t, t, '.')
plt.plot(t, t**2, '.')
ax.text(0., 60., 'Hello', fontdict=dict(size=25))
plt.savefig(
'magic.png',
format='png',
dpi=h/magic_height*dpi,
)
run:
./magic.py 431 231
outputs:
magic.png PNG 431x231 431x231+0+0 8-bit sRGB 7923B 0.000u 0:00.000
And to see if it scales nicely:
./magic.py 1291 693
outputs:
magic.png PNG 1291x693 1291x693+0+0 8-bit sRGB 25013B 0.000u 0:00.000
So we see that this approach also does work well. The only problem I have with it is that you have to set that magic_height
parameter or equivalent.
Fixed DPI + set_size_inches
This approach gave a slightly wrong pixel size, and it makes it is hard to scale everything seamlessly.
set_size_inches.py
#!/usr/bin/env python3
import sys
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
w = int(sys.argv[1])
h = int(sys.argv[2])
fig, ax = plt.subplots()
fig.set_size_inches(w/fig.dpi, h/fig.dpi)
t = np.arange(-10., 10., 1.)
plt.plot(t, t, '.')
plt.plot(t, t**2, '.')
ax.text(
0,
60.,
'Hello',
# Keep font size fixed independently of DPI.
# https://stackoverflow.com/questions/39395616/matplotlib-change-figsize-but-keep-fontsize-constant
fontdict=dict(size=10*h/fig.dpi),
)
plt.savefig(
'set_size_inches.png',
format='png',
)
run:
./set_size_inches.py 431 231
outputs:
set_size_inches.png PNG 430x231 430x231+0+0 8-bit sRGB 8078B 0.000u 0:00.000
so the height is slightly off, and the image:
The pixel sizes are also correct if I make it 3 times larger:
./set_size_inches.py 1291 693
outputs:
set_size_inches.png PNG 1291x693 1291x693+0+0 8-bit sRGB 19798B 0.000u 0:00.000
We understand from this however that for this approach to scale nicely, you need to make every DPI-dependant setting proportional to the size in inches.
In the previous example, we only made the "Hello" text proportional, and it did retain its height between 60 and 80 as we'd expect. But everything for which we didn't do that, looks tiny, including:
SVG
I could not find how to set it for SVG images, my approaches only worked for PNG e.g.:
get_size_svg.py
#!/usr/bin/env python3
import sys
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
height = int(sys.argv[1])
fig, ax = plt.subplots()
t = np.arange(-10., 10., 1.)
plt.plot(t, t, '.')
plt.plot(t, t**2, '.')
ax.text(0., 60., 'Hello', fontdict=dict(size=25))
plt.savefig(
'get_size_svg.svg',
format='svg',
dpi=height/fig.get_size_inches()[1]
)
run:
./get_size_svg.py 431
and the generated output contains:
<svg height="345.6pt" version="1.1" viewBox="0 0 460.8 345.6" width="460.8pt"
and identify says:
get_size_svg.svg SVG 614x461 614x461+0+0 8-bit sRGB 17094B 0.000u 0:00.000
and if I open it in Chromium 86 the browser debug tools mouse image hover confirm that height as 460.79.
But of course, since SVG is a vector format, everything should in theory scale, so you can just convert to any fixed sized format without loss of resolution, e.g.:
inkscape -h 431 get_size_svg.svg -b FFF -e get_size_svg.png
gives the exact height:
TODO regenerate image, messed up the upload somehow.
I use Inkscape instead of Imagemagick's convert
here because you need to mess with -density
as well to get sharp SVG resizes with ImageMagick:
And setting <img height=""
on the HTML should also just work for the browser.
Tested on matplotlib==3.2.2.