In this first post, I'll look at subplots and annotations.
Turn off axes
We can remove axes, ticks, borders, etc. with
ax.axis('off')
.
This can be used to remove unnecessary subplots created with the subplots
function
from pyplot
,
for instance, when we only want to use the upper triangle of the grid. Here's an example:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import matplotlib.pyplot as plt | |
import numpy as np | |
import scipy.stats as sts | |
## create some random data | |
n = 3 | |
Sigma = sts.wishart.rvs(scale=np.eye(n), df=n) | |
xs = sts.multivariate_normal.rvs(cov=Sigma, size=1000) | |
## plot histograms and scatter plots | |
fig, axs = plt.subplots(n, n, figsize=(7,7)) | |
for i in range(n): | |
for j in range(n): | |
if i > j: | |
axs[i, j].axis('off') | |
elif i == j: | |
axs[i, j].hist(xs[:, i], density=True) | |
else: | |
axs[i, j].scatter(xs[:, i], xs[:, j], s=5) | |
fig.tight_layout() | |
fig.savefig("axis-off.png", bbox_inches='tight', dpi=200) |
The result is:
Share axes between subplots after plotting
An alternative way of making a ragged array of subplots makes use of
gridspec
.
The problem here is that it is a bit more difficult to share x and y axes.
Of course add_subplot
has keyword arguments sharex
and sharey
,
but then we have to distinguish between the first and subsequent subplots. A better solution is the
get_shared_x_axes()
method. Here's an example:
The result is:
Hybrid (or blended) transforms for annotations
When you annotate a point in a plot, the location of the text is often relative to the data in one coordinate, but relative to the axis (e.g. in the middle) in the other. I used to do this with inverse transforms, but it turns out that there is a better way: the
blended_transform_factory
function from matplotlib.transforms
.
Suppose that I want to annotate three points in three subplots. The arrows should point to these three points, and I want the text to be located above the point, but the text in the 3 subplots has to be vertically aligned.
Notice that the y-axes are not shared between the subplots! To accomplish the alignment, we have to use the
annotate
method with a custom transform
.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import matplotlib.pyplot as plt | |
import numpy as np | |
## import the function we'll need for making the transforms | |
from matplotlib.transforms import blended_transform_factory | |
fig, axs = plt.subplots(1, 3, figsize=(10,3), sharex=True) | |
fs = [ | |
lambda x: np.sqrt(x**3), | |
lambda x: np.sqrt(x**2*(x+1)), | |
lambda x: np.sqrt((x+1)*x*(x-1)) | |
] | |
## plot some elliptic curves (the real part at least) | |
xs = np.linspace(-1, 2, 1000) | |
for f, a in zip(fs, axs): | |
a.plot(xs, f(xs), color='k', linewidth=2) | |
a.plot(xs, -f(xs), color='k', linewidth=2) | |
def hybrid_trans(ax): | |
""" | |
This function returns a blended/hybrid | |
transformation w.r.t. subplot ax. | |
x-coord: use the data for positioning | |
y-coord: use relative position w.r.t y-axis | |
""" | |
return blended_transform_factory(ax.transData, ax.transAxes) | |
## avoid repetition: common key-word arguments for annotate | |
kwargs = { | |
"arrowprops" : {"arrowstyle": "->"}, | |
"ha" : "center", | |
"va" : "bottom" | |
} | |
## give subplots names to avoid indexing | |
ax, bx, cx = axs | |
## the argument xytext determines where the text is positioned, | |
## and we can pass a transformation with textcoords. | |
## we'll use our custom "hybrid" transform so that the x-coord | |
## corresponds to the data, and the y-coord is relative. | |
ax.annotate("cusp", xy=(0,0), xytext=(0, 0.8), | |
textcoords=hybrid_trans(ax), **kwargs) | |
bx.annotate("singularity", xy=(0,0), xytext=(0, 0.8), | |
textcoords=hybrid_trans(bx), **kwargs) | |
## this is a bit of a hack: I want two annotate two points | |
## with one text box. I'll make the second text box invisible | |
## by setting alpha=0 | |
text = "two real components" | |
xcoords = [-0.25, 1.1] | |
alphas = [1, 0] | |
for x, alpha in zip(xcoords, alphas): | |
## use fs[2] to get the correct point | |
cx.annotate(text, xy=(x,fs[2](x)), xytext=(0,0.8), alpha=alpha, | |
textcoords=hybrid_trans(cx), **kwargs) | |
## add some labels and titles... | |
bx.set_xlabel("x") | |
ax.set_ylabel("y") | |
bx.set_title("three elliptic curves") | |
fig.savefig("annotations.png", bbox_inches='tight', dpi=200) |
Thanks Christiaan! This is super useful. I find myself looking for many of the same things, it is great to have it consolidated in one place.
ReplyDelete