Add external margins with constrained layout?
Question:
When generating a figure to save to a pdf file, I’d like to adjust the positioning of the figure relative to the edges of the page, for example to add an inch margin along all sides. As far as I can tell, the solutions to do this (for example, in this question) either:
- don’t work with
constrained_layout
mode — applying plt.subplots_adjust()
after creating the figure but prior to fig.savefig()
messes up the constrained layout
- don’t actually quantitatively adjust the positioning of the figure — adding
bbox_inches="tight"
or pad=-1
don’t seem to do anything meaningful
Is there a straightforward way to adjust external margins of a constrained layout figure?
For example:
fig = plt.figure(constrained_layout=True, figsize=(11, 8.5))
page_grid = gridspec.GridSpec(nrows=2, ncols=1, figure=fig)
# this doesn't appear to do anything with constrained_layout=True
page_grid.update(left=0.2, right=0.8, bottom=0.2, top=0.8)
top_row_grid = gridspec.GridSpecFromSubplotSpec(1, 3, subplot_spec=page_grid[0])
for i in range(3):
ax = fig.add_subplot(top_row_grid[:, i], aspect="equal")
n_bottom_row_plots = 10
qc_grid = gridspec.GridSpecFromSubplotSpec(1, n_bottom_row_plots, subplot_spec=page_grid[1])
for i, metric in enumerate(range(n_bottom_row_plots)):
ax = fig.add_subplot(qc_grid[:, i])
plt.plot(np.arange(5), np.arange(5))
fig.suptitle("my big label", fontweight="bold", fontsize="x-large", y=0.9)
# this ruins the constrained layout
# plt.subplots_adjust(left=0.2,right=0.8, bottom=0.2, top=0.8)
fig.savefig("temp.png", facecolor="coral")
Yields the following (I’d like to see more coral around the edges!):
Answers:
If you want to achieve more flexibility, I personally dont recommend using constrained layout and specifying [left
, right
, bottom
, top
] together. Either specify the margin by yourself, or let matplotlib contrained layout do the rearangement for you. For more space between axes for placing texts and labels, just use hspace
and wspace
to adjust.
If I want more margin from both sides, and have enough space for axes labels, tick labels, and some texts. I do the following way.
fig = plt.figure(figsize=(11, 8.5), facecolor='coral')
# you code already has this
left, right, bottom, top = [0.1, 0.95, 0.1, 0.5]
# You can specify wspace and hspace to hold axes labels and some other texts.
wspace = 0.25
hspace = 0.1
nrows=1
ncols=3
gs1 = fig.add_gridspec(nrows=1, ncols=3, left=left, right=right, bottom=0.6,
top=0.9, wspace=wspace, hspace=hspace)
axes1 = [fig.add_subplot(gs1[row, col]) for row in range(nrows) for col in
range(ncols)]
nrows=1
ncols=10
# this grid have larger wspace than gs1
gs2 = fig.add_gridspec(nrows=1, ncols=ncols, left=left, right=right,
bottom=0.1, top=top, wspace=0.6, hspace=hspace)
axes2 = [fig.add_subplot(gs2[row, col]) for row in range(nrows) for col in
range(ncols)]
Have you tried using the constrained layout padding option?
fig.set_constrained_layout_pads(w_pad=4./72., h_pad=4./72.,
hspace=0./72., wspace=0./72.)
While this might help with spacing, the constrained layout will restrict the amount of object that you can add to the defined space.
''' Here is the modified code '''
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import matplotlib.gridspec as gridspec
import numpy as np
fig = plt.figure(constrained_layout=True, figsize=(11, 8.5))
fig.set_constrained_layout_pads(w_pad=2./12., h_pad=4./12.,
hspace=0., wspace=0.)
page_grid = gridspec.GridSpec(nrows=2, ncols=1, figure=fig)
fig.suptitle("My BIG Label", fontweight="bold", fontsize="x-large", y=0.98)
# this doesn't appear to do anything with constrained_layout=True
page_grid.update(left=0.2, right=0.8, bottom=0.2, top=0.8)
top_row_grid = gridspec.GridSpecFromSubplotSpec(1, 3, subplot_spec=page_grid[0])
for i in range(3):
ax = fig.add_subplot(top_row_grid[:, i], aspect="equal")
n_bottom_row_plots = 10
qc_grid = gridspec.GridSpecFromSubplotSpec(1, n_bottom_row_plots, subplot_spec=page_grid[1])
for i, metric in enumerate(range(n_bottom_row_plots)):
ax = fig.add_subplot(qc_grid[:, i])
plt.plot(np.arange(5), np.arange(5))
# this ruins the constrained layout
# plt.subplots_adjust(left=0.2,right=0.8, bottom=0.2, top=0.8)
fig.savefig("temp.png", facecolor="coral")
You can set the rectangle that the layout engine operates within. See the rect
parameter for each engine at https://matplotlib.org/stable/api/layout_engine_api.html.
It’s unfortunately not a very friendly part of the API, especially because TightLayoutEngine
and ConstrainedLayoutEngine
have different semantics for rect
: TightLayoutEngine
uses rect = (left, bottom, right, top)
and ConstrainedLayoutEngine
uses rect = (left, bottom, width, height)
.
def set_margins(fig, margins):
"""Set figure margins as [left, right, top, bottom] in inches
from the edges of the figure."""
left,right,top,bottom = margins
width, height = fig.get_size_inches()
#convert to figure coordinates:
left, right = left/width, 1-right/width
bottom, top = bottom/height, 1-top/height
#get the layout engine and convert to its desired format
engine = fig.get_layout_engine()
if isinstance(engine, matplotlib.layout_engine.TightLayoutEngine):
rect = (left, bottom, right, top)
elif isinstance(engine, matplotlib.layout_engine.ConstrainedLayoutEngine):
rect = (left, bottom, right-left, top-bottom)
else:
raise RuntimeError('Cannot adjust margins of unsupported layout engine')
#set and recompute the layout
engine.set(rect=rect)
engine.execute(fig)
With your example:
fig = plt.figure(constrained_layout=True, figsize=(11, 8.5))
page_grid = gridspec.GridSpec(nrows=2, ncols=1, figure=fig)
#your margins were [0.2, 0.8, 0.2, 0.8] in figure coordinates
#which are 0.2*11 and 0.2*8.5 in inches from the edge
set_margins(fig,[0.2*11, 0.2*11, 0.2*8.5, 0.2*8.5])
top_row_grid = gridspec.GridSpecFromSubplotSpec(1, 3, subplot_spec=page_grid[0])
for i in range(3):
ax = fig.add_subplot(top_row_grid[:, i], aspect="equal")
n_bottom_row_plots = 10
qc_grid = gridspec.GridSpecFromSubplotSpec(1, n_bottom_row_plots, subplot_spec=page_grid[1])
for i, metric in enumerate(range(n_bottom_row_plots)):
ax = fig.add_subplot(qc_grid[:, i])
plt.plot(np.arange(5), np.arange(5))
fig.suptitle("my big label", fontweight="bold", fontsize="x-large", y=0.9)
fig.savefig("temp.png", facecolor="coral")
Note: fig.suptitle
text is apparently not handled by the layout engine, so it doesn’t move.
If what you want to do is add a margin around the finished figure, and you are not concerned with how it looks on screen, another option is to set pad_inches with bbox_inches=’tight’.
import matplotlib.pyplot as plt
fig = plt.figure(constrained_layout=True, figsize=(11, 8.5))
fig.suptitle("My BIG Label", fontweight="bold", fontsize="x-large")
sfigs = fig.subfigures(2, 1)
# top subfigure
ax = sfigs[0].subplots(1, 3)
# bottom subfigure
n_bottom_row_plots = 10
axs = sfigs[1].subplots(1, n_bottom_row_plots)
fig.savefig("temp.png", facecolor="coral", bbox_inches='tight', pad_inches=1)
There are few things in the above comment and answers to note – first constrained layout indeed deals with subtitles, however, it doesn’t if you manually set y
because it assumes if you placed it manually that is where you would like it.
Second, there is a rect
argument for the layout, but the suptitle doesn’t get constrained by that, because rect
is not really meant to be a constraint on decorations, but on where the axes get placed. I think this could be considered a bug, or at least unexpected behaviour and constrained layout should work just inside its rect
. However, given the ease of adding padding in saved output it’s probably not urgent, but if someone wants this to work that way, they are encouraged to open a bug report.
Finally, I modernized the above to use subfigures which are meant to be easier to work with than nested subplotspecs. Note each subfigure can also have a suptitle, etc
When generating a figure to save to a pdf file, I’d like to adjust the positioning of the figure relative to the edges of the page, for example to add an inch margin along all sides. As far as I can tell, the solutions to do this (for example, in this question) either:
- don’t work with
constrained_layout
mode — applyingplt.subplots_adjust()
after creating the figure but prior tofig.savefig()
messes up the constrained layout - don’t actually quantitatively adjust the positioning of the figure — adding
bbox_inches="tight"
orpad=-1
don’t seem to do anything meaningful
Is there a straightforward way to adjust external margins of a constrained layout figure?
For example:
fig = plt.figure(constrained_layout=True, figsize=(11, 8.5))
page_grid = gridspec.GridSpec(nrows=2, ncols=1, figure=fig)
# this doesn't appear to do anything with constrained_layout=True
page_grid.update(left=0.2, right=0.8, bottom=0.2, top=0.8)
top_row_grid = gridspec.GridSpecFromSubplotSpec(1, 3, subplot_spec=page_grid[0])
for i in range(3):
ax = fig.add_subplot(top_row_grid[:, i], aspect="equal")
n_bottom_row_plots = 10
qc_grid = gridspec.GridSpecFromSubplotSpec(1, n_bottom_row_plots, subplot_spec=page_grid[1])
for i, metric in enumerate(range(n_bottom_row_plots)):
ax = fig.add_subplot(qc_grid[:, i])
plt.plot(np.arange(5), np.arange(5))
fig.suptitle("my big label", fontweight="bold", fontsize="x-large", y=0.9)
# this ruins the constrained layout
# plt.subplots_adjust(left=0.2,right=0.8, bottom=0.2, top=0.8)
fig.savefig("temp.png", facecolor="coral")
Yields the following (I’d like to see more coral around the edges!):
If you want to achieve more flexibility, I personally dont recommend using constrained layout and specifying [left
, right
, bottom
, top
] together. Either specify the margin by yourself, or let matplotlib contrained layout do the rearangement for you. For more space between axes for placing texts and labels, just use hspace
and wspace
to adjust.
If I want more margin from both sides, and have enough space for axes labels, tick labels, and some texts. I do the following way.
fig = plt.figure(figsize=(11, 8.5), facecolor='coral')
# you code already has this
left, right, bottom, top = [0.1, 0.95, 0.1, 0.5]
# You can specify wspace and hspace to hold axes labels and some other texts.
wspace = 0.25
hspace = 0.1
nrows=1
ncols=3
gs1 = fig.add_gridspec(nrows=1, ncols=3, left=left, right=right, bottom=0.6,
top=0.9, wspace=wspace, hspace=hspace)
axes1 = [fig.add_subplot(gs1[row, col]) for row in range(nrows) for col in
range(ncols)]
nrows=1
ncols=10
# this grid have larger wspace than gs1
gs2 = fig.add_gridspec(nrows=1, ncols=ncols, left=left, right=right,
bottom=0.1, top=top, wspace=0.6, hspace=hspace)
axes2 = [fig.add_subplot(gs2[row, col]) for row in range(nrows) for col in
range(ncols)]
Have you tried using the constrained layout padding option?
fig.set_constrained_layout_pads(w_pad=4./72., h_pad=4./72.,
hspace=0./72., wspace=0./72.)
While this might help with spacing, the constrained layout will restrict the amount of object that you can add to the defined space.
''' Here is the modified code '''
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import matplotlib.gridspec as gridspec
import numpy as np
fig = plt.figure(constrained_layout=True, figsize=(11, 8.5))
fig.set_constrained_layout_pads(w_pad=2./12., h_pad=4./12.,
hspace=0., wspace=0.)
page_grid = gridspec.GridSpec(nrows=2, ncols=1, figure=fig)
fig.suptitle("My BIG Label", fontweight="bold", fontsize="x-large", y=0.98)
# this doesn't appear to do anything with constrained_layout=True
page_grid.update(left=0.2, right=0.8, bottom=0.2, top=0.8)
top_row_grid = gridspec.GridSpecFromSubplotSpec(1, 3, subplot_spec=page_grid[0])
for i in range(3):
ax = fig.add_subplot(top_row_grid[:, i], aspect="equal")
n_bottom_row_plots = 10
qc_grid = gridspec.GridSpecFromSubplotSpec(1, n_bottom_row_plots, subplot_spec=page_grid[1])
for i, metric in enumerate(range(n_bottom_row_plots)):
ax = fig.add_subplot(qc_grid[:, i])
plt.plot(np.arange(5), np.arange(5))
# this ruins the constrained layout
# plt.subplots_adjust(left=0.2,right=0.8, bottom=0.2, top=0.8)
fig.savefig("temp.png", facecolor="coral")
You can set the rectangle that the layout engine operates within. See the rect
parameter for each engine at https://matplotlib.org/stable/api/layout_engine_api.html.
It’s unfortunately not a very friendly part of the API, especially because TightLayoutEngine
and ConstrainedLayoutEngine
have different semantics for rect
: TightLayoutEngine
uses rect = (left, bottom, right, top)
and ConstrainedLayoutEngine
uses rect = (left, bottom, width, height)
.
def set_margins(fig, margins):
"""Set figure margins as [left, right, top, bottom] in inches
from the edges of the figure."""
left,right,top,bottom = margins
width, height = fig.get_size_inches()
#convert to figure coordinates:
left, right = left/width, 1-right/width
bottom, top = bottom/height, 1-top/height
#get the layout engine and convert to its desired format
engine = fig.get_layout_engine()
if isinstance(engine, matplotlib.layout_engine.TightLayoutEngine):
rect = (left, bottom, right, top)
elif isinstance(engine, matplotlib.layout_engine.ConstrainedLayoutEngine):
rect = (left, bottom, right-left, top-bottom)
else:
raise RuntimeError('Cannot adjust margins of unsupported layout engine')
#set and recompute the layout
engine.set(rect=rect)
engine.execute(fig)
With your example:
fig = plt.figure(constrained_layout=True, figsize=(11, 8.5))
page_grid = gridspec.GridSpec(nrows=2, ncols=1, figure=fig)
#your margins were [0.2, 0.8, 0.2, 0.8] in figure coordinates
#which are 0.2*11 and 0.2*8.5 in inches from the edge
set_margins(fig,[0.2*11, 0.2*11, 0.2*8.5, 0.2*8.5])
top_row_grid = gridspec.GridSpecFromSubplotSpec(1, 3, subplot_spec=page_grid[0])
for i in range(3):
ax = fig.add_subplot(top_row_grid[:, i], aspect="equal")
n_bottom_row_plots = 10
qc_grid = gridspec.GridSpecFromSubplotSpec(1, n_bottom_row_plots, subplot_spec=page_grid[1])
for i, metric in enumerate(range(n_bottom_row_plots)):
ax = fig.add_subplot(qc_grid[:, i])
plt.plot(np.arange(5), np.arange(5))
fig.suptitle("my big label", fontweight="bold", fontsize="x-large", y=0.9)
fig.savefig("temp.png", facecolor="coral")
Note: fig.suptitle
text is apparently not handled by the layout engine, so it doesn’t move.
If what you want to do is add a margin around the finished figure, and you are not concerned with how it looks on screen, another option is to set pad_inches with bbox_inches=’tight’.
import matplotlib.pyplot as plt
fig = plt.figure(constrained_layout=True, figsize=(11, 8.5))
fig.suptitle("My BIG Label", fontweight="bold", fontsize="x-large")
sfigs = fig.subfigures(2, 1)
# top subfigure
ax = sfigs[0].subplots(1, 3)
# bottom subfigure
n_bottom_row_plots = 10
axs = sfigs[1].subplots(1, n_bottom_row_plots)
fig.savefig("temp.png", facecolor="coral", bbox_inches='tight', pad_inches=1)
There are few things in the above comment and answers to note – first constrained layout indeed deals with subtitles, however, it doesn’t if you manually set y
because it assumes if you placed it manually that is where you would like it.
Second, there is a rect
argument for the layout, but the suptitle doesn’t get constrained by that, because rect
is not really meant to be a constraint on decorations, but on where the axes get placed. I think this could be considered a bug, or at least unexpected behaviour and constrained layout should work just inside its rect
. However, given the ease of adding padding in saved output it’s probably not urgent, but if someone wants this to work that way, they are encouraged to open a bug report.
Finally, I modernized the above to use subfigures which are meant to be easier to work with than nested subplotspecs. Note each subfigure can also have a suptitle, etc