TL;DR: This page describes an approach to composing figures in Python’s matplotlib
. The writing is messy, but I think the main idea is clear enough to be useful.
Composing Figures in matplotlib can be challenging.
Options include
subplots
(Axes within a Figure, created from methods on Figure instances), andgridspecs
(also subplots within a figure, created using a more expressive gridspec syntax).
These are ok, but there are some aspects of these solutions that I don’t particularly like.
Research Figures often need:
- Complex layouts - not always needed but nice to know it’s possible.
- Modularity - figures standalone (ie for a presentation slide) and composed (ie for a manuscript).
- Often we want to maintain standalone Figure A, standalone Figure B, and a composed Figure A+B.
- Provenance & editability - the method described here doesn’t solve for this, but lays a foundation.
Existing Approaches for Composing Figures:
- If we go for composition of Axes within a Figure, I have never understood how to untangle one Axis from the rest of the Figure. It would be great to be able to do this so we a can access Figure API stuff, like savefig, to do things to just Figure A.
- The other choice is standalone Figures. It wasn’t clear to me how to compose these. The naive solution would be to collect a bunch of Figure objects then somehow stitch them together. Something like
I haven’t found a straightforward way to do this. Others have asked:
https://stackoverflow.com/questions/45810557/copy-an-axes-content-and-show-it-in-a-new-figure
Solutions in the stackoverflow post use pickle
...
inx = list(fig.axes).index(event.inaxes)
buf = io.BytesIO()
pickle.dump(fig, buf)
buf.seek(0)
fig2 = pickle.load(buf)
for i, ax in enumerate(fig2.axes):
if i != inx:
fig2.delaxes(ax)
else:
axes=ax
axes.change_geometry(1,1,1)
fig2.show()
...
, or pull data out of the Figure object:
These solutions don’t feel very direct or readable.
- I don’t find the gridspec syntax very readable.
This is subjective, but here is an example of some gridspec code from the matplotlib docs:
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
def format_axes(fig):
for i, ax in enumerate(fig.axes):
ax.text(0.5, 0.5, "ax%d" % (i+1), va="center", ha="center")
ax.tick_params(labelbottom=False, labelleft=False)
# gridspec inside gridspec
fig = plt.figure()
gs0 = gridspec.GridSpec(1, 2, figure=fig)
gs00 = gridspec.GridSpecFromSubplotSpec(3, 3, subplot_spec=gs0[0])
ax1 = fig.add_subplot(gs00[:-1, :])
ax2 = fig.add_subplot(gs00[-1, :-1])
ax3 = fig.add_subplot(gs00[-1, -1])
# the following syntax does the same as the GridSpecFromSubplotSpec call above:
gs01 = gs0[1].subgridspec(3, 3)
ax4 = fig.add_subplot(gs01[:, :-1])
ax5 = fig.add_subplot(gs01[:-1, -1])
ax6 = fig.add_subplot(gs01[-1, -1])
plt.suptitle("GridSpec Inside GridSpec")
format_axes(fig)
plt.show()
A Different Approach
I want to highlight a less-emphasized way of composing figures in matplotlib. This isn’t new, but I don’t see it used or discussed often.
To do this, we’ll use some techniques from functional programming.
One of them is partial function application. Another is first-class functions.
This is also a chance to use some less-known features of Python typing, like Protocol.
Part 1: Background
Let’s read the docs here:
https://matplotlib.org/stable/gallery/subplots_axes_and_figures/subfigures.html
We can see there is a way to nest visualizations at the Figure level, instead of the Axes level.
https://matplotlib.org/stable/_images/sphx_glr_subfigures_004_2_00x.png
Part 2: Strategy
Instead of trying to compose figure objects, we’ll compose the functions that create them.
We’ll define functions that accept a Figure (expected to be empty), some data, and other keyword arguments, and return a Figure.
How to compose them: We need a function to orchestrate the composition of the other functions.
Let’s call this compose_on_subfigures
.
This is where first-class functions come in — we’ll pass this function a list of the visualizer functions from before.
compose_on_subfigures
looks like this:
It takes a Figure, some data, some keyword arguments that dictate how to split up fig into SubFigures, and a list of subfigure_fns.
Each of the subfigure_fns is a Callable that should accept two arguments, fig and data, plus arbitrary keyword arguments, and return a Figure.
Notice anything? The function signature for compose_on_subfigures
, and the function signature for a subfig_fn
are almost the same! This allows us to nest calls to compose_on_subfigures
to achieve any nested grid layout we want. And because each of the subfigure_fns
return Figure
s (instead of Axes
), we can also use this completely standalone.
More precise typing with Protocol
If we want to enhance the level of specificity of typing for the subfigure_fns argument, we have some options:
Callable
or
Callable[Figure,DataFrame] -> Figure
The latter is pretty good. Some downsides though:
- it indicates typing by position. Not necessarily a downside but not clear whether it will support calls like
my_figure_fn(data,fig)
andmy_figure_fn(fig,data)
equally well. Haven’t checked but it probably shouldn’t.
Also, I don’t think **kwargs
is supported in the signature for Callable.
For more strict typing, we can use Protocol from the typing module.
This type annotation can be used like: