part of Course 133 Navigating Matplotlib
Get your layout right
I've spent more time wrestling with frames, layouts, and backgrounds than anything else in Matplotlib. Here are the most straightforward methods I've found.
First, we'll start with this setup for all the examples to follow. For an explanation of it, check here.
import matplotlib
matplotlib.use("agg")
import matplotlib.pyplot as plt
import numpy as np
# Create the curve data.
x = np.linspace(-6, 6, 500)
y = np.sinc(x)
fig = plt.figure()
ax = fig.gca()
ax.plot(x, y)
Manually place your Axes
By default, a Figure will create an Axes object with a reasonable size and location, but it doesn't always get it right. If you change your tick label formatting or tick font size for instance, things can start falling off the edge of the Figure. To get around this, you can manually place Axes right where you wnt them.
ax = fig.add_axes((left, bottom, width, height))
This call to add_axes()
specifies the x and y positions
of the lower left hand corner of the Axes, together with
the width and the height of it.
It's most important to note that all of these are expressed as
fractions of the figure width and height. Because of this, they will
almost always fall between 0 and 1.
Create several Axes
Some stories are best told by putting two or more plots side by side. In Matplotlib you can do this by adding additional Axes to the same Figure as many times as you need to.
ax1 = fig.add_axes((left_1, bottom_1, width_1, height_1))
ax1.plot(x, y)
ax2 = fig.add_axes((left_2, bottom_2, width_2, height_2))
ax2.plot(x, y)
...
Each one is a separate object and can be modified independently using all the methods we've mentioned already. Axes can overlap each other and even fall partly outside the Figure boundaries.
I have to admit a bias here. There are already some tools in place to
help layout multiple Axes objects. There's
subplots()
and
gridspec()
. Those get 80% of the way there.
Then there is
tight_layout()
and
constrained layout()
which try to clean things up
and make up yet another 10%. If you need a 90% solution, these are
a fine way to go. But if you want things to look a particular way
or line up just so, then you'll soon find yourself tweaking these
and fiddling with down-in-the-weeds settings. In my experience, I
get better results with less work by thinking through the layout I
want and specifying each Axes manually. But if you are
feeling curious try both approaches for yourself and see what works
best for you.
Fill the Figure with one Axes
One trick that's occasionally useful is to fill the entire Figure with the plot area of one Axes. With our trick of manual placement, this is a breeze.
ax1 = fig.add_axes((0, 0, 1, 1))
Using left and bottom values of 0 and width and height of 1 fills the Axes completely, by definition.
Hide axis spines
Removing unnecessary lines really cleans up a plot. The lines that form the border of the plot area are called axis spines, and sometimes they are expendable.
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
ax.spines["left"].set_visible(False)
This trick lets us set the properties of each of the four spines independently. Here, we toggle their visibility.
Move the spines around
We can also shift the spines around a bit. The y-axis spines can move right and left and the x-axis spines can move up and down.
ax.spines["top"].set_position(("outward", 15))
ax.spines["bottom"].set_position(("data", 0))
ax.spines["left"].set_position(("axes", .3))
ax.spines["right"].set_position(("outward", -40))
There are three different methods to move them: "data"
,
"axes"
, and "outward"
.
"data"
lets you specify the position in your data
coordinates to place the spine. This is super useful for putting
spines along the x = 0 and y = 0 lines.
"axes"
lets you name the position as a fraction of the
Axes height and width. 0 is the bottom or the left of the
plotting area, and 1 is the top or the right. .5 is right in the
middle. Less than 0 or more than 1 is outside the plotting area.
"outward"
lets you indicate a position as an offset from
the boundary of the plotting area, measured in points (there are 72
points in an inch). A positive offset is away from the center of the
plotting area, a negative offset is toward it. A positive offset for a
left spine would move it to the left, and a positive offset of a right
spine would move it to the right, for example.
Note that all of these let you place spines outside the plotting area (or even outside the Figure boundaries).
Modify the spines
Just like all the other lines in our plot, we can modify their style to suit our whims.
ax.spines["top"].set_color("orange")
ax.spines["left"].set_bounds(-.5, .5)
ax.spines["right"].set_linestyle("--")
ax.spines["bottom"].set_linewidth(6)
ax.spines["bottom"].set_capstyle("round")
Most of the settings should be familiar from the
section on Lines, but it's worth
calling out set_bounds()
. It allows you to dictate the
lower and upper bounds of an axis spine. There's no need for it to
span the plotting area.
Change the size of the Figure
You can also change the measurements of the Figure overall. When you create it without choosing a size, Matplotlib chooses a reasonable default size - typically something like 6 1/2 inches wide by 4 1/2 inches high. However, if you know the final image is going to need to be a certain size, say, filling a 12 x 8 inch spot on a poster or a 3 x 2 inch slot in a two-column paper manuscript, if can be helpful to start with that.
fig = plt.figure(figsize=(width, height))
Here, width
and height
are always in
inches. By starting with the final size specification, you can avoid
rescaling your final image. This lets you control the aspect ratio
and makes the font sizes and line thicknesses meaningful. It also
ensures that you can pick an output resolution that will look sharp.
Change the resolution of the Figure
Together with size, controlling the output image resolution gives you control over the quality of the final product.
fig.savefig("image_filename.png", dpi=15)
You can specify the dpi
, dots per inch, in the call to
savefig()
. For printing and most screens, 150 is pretty
good, 300 is clear, and 600 is spectacular. 1200 or higher can come
in handy if you want to be able to do a lot of zooming in, but your
image can start to get very big on disk at that resolution.
Change Axes background color
It's not tough to change up the background color of the plotting area.
ax.set_facecolor("#e1ddbf")
You can use any Matplotlib color specification for this. Here we used a hexidecimal code.
Change Figure background color
Similarly, you can tweak the Figure background color to be whatever you like.
fig = plt.figure(facecolor="#e1ddbf")
fig.savefig("image_filename.png", facecolor=fig.get_facecolor())
We can set this up when first creating the Figure by
including the facecolor
argument and choosing a color.
One quirk of this is that savefig()
tries to be helpful
and assumes that you'd like to save with a white background. While
this default has doubtless saved truckloads of toner cartridges, it's
not always what you want. To get around this, you can manually
re-specify that the facecolor
of the saved image
background should definitely be the facecolor
of the
original Figure.
Frame the Figure
Rounding out our menu of layout midifications is a frame around the whole Figure.
fig = plt.figure(linewidth=10, edgecolor="#04253a")
fig.savefig("image_filename.png", edgecolor=fig.get_edgecolor())
We can create a border by both choosing a nonzero
linewidth
and an edgecolor
when creating our
Figure. When calling savefig()
we have to
repeat our trick of explicitly setting the edgecolor
in
the output image.
Now you know all my layout tricks. As with all of Matplotlib, there are several ways to do most of these things, but what I included here are for me the most intuitive, most flexible, and require the least memorization.
If this was helpful, there are a handful of other Matplotlib how-to's you can refer to: come take a look at the full set of tutorials.