1
votes

Let's take this as a starting point based on: Specifying and saving a figure with exact size in pixels

#!/usr/bin/env python3

import sys

import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl

h = int(sys.argv[1])
fig, ax = plt.subplots(nrows=2, ncols=1)
t = np.arange(-10., 10., 1.)
a = ax[0]
a.set_aspect(1)
a.plot(t, t, '.')
a = ax[1]
a.plot(t, -t, '.')
a.set_aspect(1)
plt.savefig(
    'main.png',
    format='png',
    dpi=h/fig.get_size_inches()[1],
    facecolor='y',
)

which allows me to do:

./main.py 400 && identify main.png

to generate an image with the correct desired height of 400 pixels:

main.png PNG 533x400 533x400+0+0 8-bit sRGB 6058B 0.000u 0:00.000

enter image description here

However, there's a lot of empty space to the left and right of the plots. This space is due to the fixed aspect ratio of 1 that I also want (x and y data have the same sizes). We can confirm that by removing the set_aspect calls, which gives a figure with reasonably sized margins:

enter image description here

but I want the 1/1 aspect ratio as well.

I've tried to remove this space with several methods from: Removing white space around a saved image in matplotlib but none gave what I wanted.

For example, if I add:

plt.savefig(bbox_inches='tight',

I get the desired image:

enter image description here

but the height is not 400 anymore as I want it to be:

main.png PNG 189x345 189x345+0+0 8-bit sRGB 4792B 0.000u 0:00.000

Or if I try instead to add:

plt.tight_layout(pad=0)

the height is correct, but it didn't remove the horizontal space:

enter image description here

One thing I could do is to explicitly set the width as in:

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(nrows=2, ncols=1)
wi, hi = fig.get_size_inches()
fig.set_size_inches(hi*(w/h), hi)
t = np.arange(-10., 10., 1.)
a = ax[0]
a.set_aspect(1)
a.plot(t, t, '.')
a = ax[1]
a.plot(t, -t, '.')
a.set_aspect(1)
plt.tight_layout(pad=1)
plt.savefig(
    'main.png',
    format='png',
    dpi=h/hi,
    facecolor='y',
)

and run that with:

./main.py 400 250 && identify main.png

where 250 is selected by trial and error, and that does give the exact pixel dimensions and good looking output:

enter image description here

but I'd rather not have to do the trial and error to find the value 250, I want that to be determined automatically by matplotlib.

This might be what matplotlib: Set width or height of figure without changing aspect ratio is asking, but it is hard to be sure without concrete examples.

Tested on matplotlib==3.2.2.

2

2 Answers

0
votes

I don't know if I understood your question, but if you want to limit the whitespace in a figure with a 1x2 subplot layout, you simply have to create a figure with a width that's half the height:

h = 400
nrows = 2
w = h/nrows
dpi = 100

fig, ax = plt.subplots(nrows=nrows, ncols=1, figsize=(w/dpi, h/dpi), dpi=dpi)

t = np.arange(-10., 10., 1.)
a = ax[0]
a.set_aspect(1)
a.plot(t, t, '.')
a = ax[1]
a.plot(t, -t, '.')
a.set_aspect(1)
plt.tight_layout(pad=1)
plt.savefig(
    'main.png',
    format='png',
    dpi=dpi,
    facecolor='y',
)

>> identify main.png

main.png PNG 200x400 200x400+0+0 8-bit sRGB 6048B 0.000u 0:00.000

enter image description here

0
votes

SVG output + plt.savefig(bbox_inches='tight' + Inkscape convert

This is terrible, but it does what I want without a lot of extra boilerplate, so here we go:

#!/usr/bin/env python3

import sys

import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl

h = int(sys.argv[1])
fig, ax = plt.subplots(nrows=2, ncols=1)
t = np.arange(-10., 10., 1.)
a = ax[0]
a.set_aspect(1)
a.plot(t, t, '.')
a = ax[1]
a.plot(t, -t, '.')
a.set_aspect(1)
plt.savefig(
    'main.svg',
    format='svg',
    dpi=h/fig.get_size_inches()[1],
    facecolor='y',
    bbox_inches='tight',
)

and then:

inkscape -b FFF -e main.png -h 400 main.svg

output:

enter image description here

bbox_inches='tight' gives a decent looking image without too much borders, but makes me lose the exact size, so we use the SVG output as a way to work around that.

This should work well since SVG is a vector format, and therefore should scale to any size seamlessly.

I use Inkscape for the conversion because Imagemagick requires you to manually calculate the resolution:

Tested on Inkscape 0.92.5.