Matplotlib title spanning two (or any number of) subplot columns - python

Because of the nature of what I am plotting, I want subplots akin to nested tables.
I'm not sure how to ask the question clearly so I'll added some pictures instead which I hope illustrate the problem.
What I have:
What I want:
Current (shortened) code looks something like this:
fig, axes = plt.subplots(nrows=5, ncols=4)
fig.suptitle(title, fontsize='x-large')
data0.plot(x=data0.x, y=data0.y, ax=axes[0,0],kind='scatter')
data1.plot(x=data1.x, y=data1.y, ax=axes[0,1],kind='scatter')
axes[0,0].set_title('title 0')
axes[0,1].set_title('title 1')
I can't figure out how to set a title for axes[0,0] and [0,1] together. I can't find anything in the documentation either. I am not fond of fussing around with tables in latex to achieve this. Any pointers?

Setting the figure title using fig.suptitle() and the axes (subplot) titles using ax.set_title() is rather straightforward. For setting an intermediate, column spanning title there is indeed no build in option.
One way to solve this issue can be to use a plt.figtext() at the appropriate positions. One needs to account some additional space for that title, e.g. by using fig.subplots_adjust and find appropriate positions of this figtext.
In the example below, we use the bounding boxes of the axes the title shall span over to find a centralized horizontal position. The vertical position is a best guess.
import matplotlib.pyplot as plt
import numpy as np
x = np.arange(10)
y = np.random.rand(10,8)
colors=["b", "g", "r", "violet"]
fig, axes = plt.subplots(nrows=2, ncols=4, sharex=True, sharey=True, figsize=(8,5))
#set a figure title on top
fig.suptitle("Very long figure title over the whole figure extent", fontsize='x-large')
# adjust the subplots, i.e. leave more space at the top to accomodate the additional titles
fig.subplots_adjust(top=0.78)
ext = []
#loop over the columns (j) and rows(i) to populate subplots
for j in range(4):
for i in range(2):
axes[i,j].scatter(x, y[:,4*i+j], c=colors[j], s=25)
# each axes in the top row gets its own axes title
axes[0,j].set_title('title {}'.format(j+1))
# save the axes bounding boxes for later use
ext.append([axes[0,j].get_window_extent().x0, axes[0,j].get_window_extent().width ])
# this is optional
# from the axes bounding boxes calculate the optimal position of the column spanning title
inv = fig.transFigure.inverted()
width_left = ext[0][0]+(ext[1][0]+ext[1][1]-ext[0][0])/2.
left_center = inv.transform( (width_left, 1) )
width_right = ext[2][0]+(ext[3][0]+ext[3][1]-ext[2][0])/2.
right_center = inv.transform( (width_right, 1) )
# set column spanning title
# the first two arguments to figtext are x and y coordinates in the figure system (0 to 1)
plt.figtext(left_center[0],0.88,"Left column spanning title", va="center", ha="center", size=15)
plt.figtext(right_center[0],0.88,"Right column spanning title", va="center", ha="center", size=15)
axes[0,0].set_ylim([0,1])
axes[0,0].set_xlim([0,10])
plt.show()

New in matplotlib 3.4.0
You can use subfigures if you have matplotlib version >= 3.4.0 (as mentioned in a comment by #ra0).
Once the subfigures are created, you can treat them exactly as you would a normal figure and create subplots and add suptitles.
Documentation and examples on subfigures.
import matplotlib.pyplot as plt
import numpy as np
x = np.arange(10)
y = np.random.rand(10, 8)
colors = ["b", "g", "r", "violet"]
fig = plt.figure(figsize=(8, 5), constrained_layout=True)
subfigs = fig.subfigures(1, 2)
titles = ["Left spanning title", "Right spanning title"]
for i, subfig in enumerate(subfigs):
axes = subfig.subplots(2, 2)
for j, row in enumerate(axes):
for k, ax in enumerate(row):
ax.scatter(x, y[:, i*4 + j*2 + k], color=colors[i*2 + k], s=25)
ax.set_xlim([0, 10])
ax.set_ylim([0, 1])
if j == 0:
ax.set_title(f"fig{i}, row{j}, col{k}")
subfig.suptitle(titles[i])
fig.suptitle("Very long figure title over the whole figure extent", fontsize='x-large')
plt.show()

Related

Change to 4 columns from 2

I have a function that plots a boxplot and a histogram side by side in two columns. I would like to change my code to make it 4 columns to shorten the output. I have played with the code and I am missing something. I can change it to 4 columns, but then the right 2 are blank and everything is in the two left columns.
I have tried changing the line to
ax_box, ax_hist, ax_box2, ax_hist2 = axs[i*ncols], axs[i*ncols+1], axs[i*ncols+2], axs[i*ncols+3]
instead of
ax_box, ax_hist = axs[i*ncols], axs[i*ncols+1]
among other iterations of changing the indexes on the columns.
I am new to python and I know I am missing something that will be obvious to more experienced people.
my code is:
`def hist_box_all1(data, bins):
ncols = 2 # Number of columns for subplots
nrows = len(data.columns) # Number of rows for subplots
height_ratios = [0.75, 0.75] * (nrows // 2) + [0.75] * (nrows % 2)
fig, axs = plt.subplots(nrows=nrows, ncols=ncols, figsize=(15,4*nrows), gridspec_kw={'height_ratios': height_ratios})
axs = axs.ravel() # Flatten the array of axes
for i, feature in enumerate(data.columns):
ax_box, ax_hist = axs[i*ncols], axs[i*ncols+1]
sns.set(font_scale=1) # Set the size of the label
x = data[feature]
n = data[feature].mean() # Get the mean for the legend
m=data[feature].median()
sns.boxplot(
x=x,
ax=ax_box,
showmeans=True,
meanprops={
"marker": "o",
"markerfacecolor": "white",
"markeredgecolor": "black",
"markersize": "7",
},
color="teal",
)
sns.histplot(
x=x,
bins=bins,
kde=True,
stat="density",
ax=ax_hist,
color="darkorchid",
edgecolor="black",
)
ax_hist.axvline(
data[feature].mean(), color="teal", label="mean=%f" % n
) # Draw the mean line
ax_hist.axvline(
data[feature].median(), color="red", label="median=%f" % m
) #Draw the median line
ax_box.set(yticks=[]) # Format the y axis label
#sns.despine(ax=ax_hist) # Remove the axis lines on the hist plot
#sns.despine(ax=ax_box, left=True) # Remove the axis lines on the box plot
ax_hist.legend(loc="upper right") # Place the legend in the upper right corner
plt.suptitle(feature)
plt.tight_layout()`
Here is a screen shot of the output
Here is a screen shot of the data
I find this way of using matplotlib.pyplot and add_subplot() more convenient to add multiple subplots in a plot.
import matplotlib.pyplot as plt
fig = plt.figure(figsize=[22, 6])
ax = fig.add_subplot(1, 2, 1) # (1,2) means 1 row 2 columns, '1' at the final index indicates the order of this subplot i.e., first subplot on left side
ax.hist(somedata_for_left_side)
ax.set_title('Title 01')
ax = fig.add_subplot(1, 2, 2) # '2' at the final index indicates the order of this subplot i.e., second subplot on the right side
ax.hist(somedata_for_right_side)
ax.set_title('Title 02')
Example output plot (different titles and plot type):
You can try to adapt this to your code. Let me know if this helps.
Since it's a bit hard to see visually what your error is. Can you add some screenshots for easier understanding?

Python, Matplotlib custom axes share Y axis

I have simple code to create a figure with 7 axes/ custom subplots (my understanding is that subplots are equal-sized and equal-spaced and in my particular situation I need one to be larger than the rest).
fig = plt.figure(figsize = (16,12))
# row 1
ax1 = plt.axes([0.1,0.7,0.2,0.2])
ax2 = plt.axes([0.4,0.7,0.2,0.2])
ax3 = plt.axes([0.7,0.7,0.2,0.2])
# big row 2
ax4 = plt.axes([0.1, 0.4, 0.5, 0.2])
#row 3
ax5 = plt.axes([0.1,0.1,0.2,0.2])
ax6 = plt.axes([0.4,0.1,0.2,0.2])
ax7 = plt.axes([0.7,0.1,0.2,0.2])
my question is, how do i get all of these axes to share the same y-axis. All i can find on google/stack is for subplots, eg:
ax = plt.subplot(blah, sharey=True)
but calling the same thing for axes creation does not work:
ax = plt.axes([blah], sharey=True) # throws error
is there anyway to accomplish this? What I'm working with is:
This is quite simple using matplotlib.gridspec.GridSpec
gs=GridSpec(3,3) creates a 3x3 grid to place subplots on
For your top and bottom rows, we just need to index one cell on that 3x3 grid (e.g. gs[0,0] is on the top left).
For the middle row, you need to span two columns, so we use gs[1,0:2]
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
fig=plt.figure(figsize=(16,12))
gs = GridSpec(3,3)
# Top row
ax1=fig.add_subplot(gs[0,0])
ax2=fig.add_subplot(gs[0,1],sharey=ax1)
ax3=fig.add_subplot(gs[0,2],sharey=ax1)
# Middle row
ax4=fig.add_subplot(gs[1,0:2],sharey=ax1)
# Bottom row
ax5=fig.add_subplot(gs[2,0],sharey=ax1)
ax6=fig.add_subplot(gs[2,1],sharey=ax1)
ax7=fig.add_subplot(gs[2,2],sharey=ax1)
ax1.set_ylim(-15,10)
plt.show()

Embedding several inset axes in another axis using matplotlib

Is it possible to embed a changing number of plots in a matplotlib axis? For example, the inset_axes method is used to place inset axes inside parent axes:
However, I have several rows of plots and I want to include some inset axes inside the last axis object of each row.
fig, ax = plt.subplots(2,4, figsize=(15,15))
for i in range(2):
ax[i][0].plot(np.random.random(40))
ax[i][2].plot(np.random.random(40))
ax[i][3].plot(np.random.random(40))
# number of inset axes
number_inset = 5
for j in range(number_inset):
ax[i][4].plot(np.random.random(40))
Here instead of the 5 plots drawn in the last column, I want several inset axes containing a plot. Something like this:
The reason for this is that every row refers to a different item to be plotted and the last column is supposed to contain the components of such item. Is there a way to do this in matplotlib or maybe an alternative way to visualize this?
Thanks
As #hitzg mentioned, the most common way to accomplish something like this is to use GridSpec. GridSpec creates an imaginary grid object that you can slice to produce subplots. It's an easy way to align fairly complex layouts that you want to follow a regular grid.
However, it may not be immediately obvious how to use it in this case. You'll need to create a GridSpec with numrows * numinsets rows by numcols columns and then create the "main" axes by slicing it with intervals of numinsets.
In the example below (2 rows, 4 columns, 3 insets), we'd slice by gs[:3, 0] to get the upper left "main" axes, gs[3:, 0] to get the lower left "main" axes, gs[:3, 1] to get the next upper axes, etc. For the insets, each one is gs[i, -1].
As a complete example:
import numpy as np
import matplotlib.pyplot as plt
def build_axes_with_insets(numrows, numcols, numinsets, **kwargs):
"""
Makes a *numrows* x *numcols* grid of subplots with *numinsets* subplots
embedded as "sub-rows" in the last column of each row.
Returns a figure object and a *numrows* x *numcols* object ndarray where
all but the last column consists of axes objects, and the last column is a
*numinsets* length object ndarray of axes objects.
"""
fig = plt.figure(**kwargs)
gs = plt.GridSpec(numrows*numinsets, numcols)
axes = np.empty([numrows, numcols], dtype=object)
for i in range(numrows):
# Add "main" axes...
for j in range(numcols - 1):
axes[i, j] = fig.add_subplot(gs[i*numinsets:(i+1)*numinsets, j])
# Add inset axes...
for k in range(numinsets):
m = k + i * numinsets
axes[i, -1][k] = fig.add_subplot(gs[m, -1])
return fig, axes
def plot(axes):
"""Recursive plotting function just to put something on each axes."""
for ax in axes.flat:
data = np.random.normal(0, 1, 100).cumsum()
try:
ax.plot(data)
ax.set(xticklabels=[], yticklabels=[])
except AttributeError:
plot(ax)
fig, axes = build_axes_with_insets(2, 4, 3, figsize=(12, 6))
plot(axes)
fig.tight_layout()
plt.show()
This is what I did to obtain the same result without setting the number of inset plots in advance.
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import numpy as np
fig = plt.figure(figsize=(12,6))
nrows = 2
ncols = 4
# changing the shape of GridSpec's output
outer_grid = gridspec.GridSpec(nrows, ncols)
grid = []
for i in range(nrows*ncols):
grid.append(outer_grid[i])
outer_grid = np.array(grid).reshape(nrows,ncols)
for i in range(nrows):
inner_grid_1 = gridspec.GridSpecFromSubplotSpec(1, 1,
subplot_spec=outer_grid[i][0])
ax = plt.Subplot(fig, inner_grid_1[0])
ax.plot(np.random.normal(0,1,50).cumsum())
fig.add_subplot(ax)
inner_grid_2 = gridspec.GridSpecFromSubplotSpec(1, 1,
subplot_spec=outer_grid[i][1])
ax2 = plt.Subplot(fig, inner_grid_2[0])
ax2.plot(np.random.normal(0,1,50).cumsum())
fig.add_subplot(ax2)
inner_grid_3 = gridspec.GridSpecFromSubplotSpec(1, 1,
subplot_spec=outer_grid[i][2])
ax3 = plt.Subplot(fig, inner_grid_3[0])
ax3.plot(np.random.normal(0,1,50).cumsum())
fig.add_subplot(ax3)
# this value can be set based on some other calculation depending
# on each row
numinsets = 3
inner_grid_4 = gridspec.GridSpecFromSubplotSpec(numinsets, 1,
subplot_spec=outer_grid[i][3])
# Adding subplots to the last inner grid
for j in range(inner_grid_4.get_geometry()[0]):
ax4 = plt.Subplot(fig, inner_grid_4[j])
ax4.plot(np.random.normal(0,1,50).cumsum())
fig.add_subplot(ax4)
# Removing labels
for ax in fig.axes:
ax.set(xticklabels=[], yticklabels=[])
fig.tight_layout()

automatically position text box in matplotlib

Is there a way of telling pyplot.text() a location like you can with pyplot.legend()?
Something like the legend argument would be excellent:
plt.legend(loc="upper left")
I am trying to label subplots with different axes using letters (e.g. "A","B"). I figure there's got to be a better way than manually estimating the position.
Thanks
Just use annotate and specify axis coordinates. For example, "upper left" would be:
plt.annotate('Something', xy=(0.05, 0.95), xycoords='axes fraction')
You could also get fancier and specify a constant offset in points:
plt.annotate('Something', xy=(0, 1), xytext=(12, -12), va='top'
xycoords='axes fraction', textcoords='offset points')
For more explanation see the examples here and the more detailed examples here.
I'm not sure if this was available when I originally posted the question but using the loc parameter can now actually be used. Below is an example:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.offsetbox import AnchoredText
# make some data
x = np.arange(10)
y = x
# set up figure and axes
f, ax = plt.subplots(1,1)
# loc works the same as it does with figures (though best doesn't work)
# pad=5 will increase the size of padding between the border and text
# borderpad=5 will increase the distance between the border and the axes
# frameon=False will remove the box around the text
anchored_text = AnchoredText("Test", loc=2)
ax.plot(x,y)
ax.add_artist(anchored_text)
plt.show()
The question is quite old but as there is no general solution to the problem till now (2019) according to Add loc=best kwarg to pyplot.text(), I'm using legend() and the following workaround to obtain auto-placement for simple text boxes:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpl_patches
x = np.linspace(-1,1)
fig, ax = plt.subplots()
ax.plot(x, x*x)
# create a list with two empty handles (or more if needed)
handles = [mpl_patches.Rectangle((0, 0), 1, 1, fc="white", ec="white",
lw=0, alpha=0)] * 2
# create the corresponding number of labels (= the text you want to display)
labels = []
labels.append("pi = {0:.4g}".format(np.pi))
labels.append("root(2) = {0:.4g}".format(np.sqrt(2)))
# create the legend, supressing the blank space of the empty line symbol and the
# padding between symbol and label by setting handlelenght and handletextpad
ax.legend(handles, labels, loc='best', fontsize='small',
fancybox=True, framealpha=0.7,
handlelength=0, handletextpad=0)
plt.show()
The general idea is to create a legend with a blank line symbol and to remove the resulting empty space afterwards. How to adjust the size of matplotlib legend box? helped me with the legend formatting.

Python Subplot function parameters

I am having a hard time with putting in the parameters for the python subplot function.
What I want is to plot 4 graphs on a same image file with the following criteria
left
space
right
space
left
space
right
I have tried different ways of the 3 numbers but the output doesnt show up correctly.
Do you mean something like this?
import matplotlib.pyplot as plt
fig = plt.figure()
ax1 = fig.add_subplot(4,2,1)
ax2 = fig.add_subplot(4,2,4)
ax3 = fig.add_subplot(4,2,5)
ax4 = fig.add_subplot(4,2,8)
fig.subplots_adjust(hspace=1)
plt.show()
Well, the not-so-easily-found documentation regarding the sublot function template is as follows:
subplot (number_of_graphs_horizontal, number of graphs_vertical, index)
Let us investigate the code from Joe Kington above:
import matplotlib.pyplot as plt
fig = plt.figure()
ax1 = fig.add_subplot(4,2,1)
ax2 = fig.add_subplot(4,2,4)
ax3 = fig.add_subplot(4,2,5)
ax4 = fig.add_subplot(4,2,8)
fig.subplots_adjust(hspace=1)
plt.show()
You told matplotlib that you want a grid with 4 rows and 2 columns of graphs. ax1, ax2 and so on are the graphs that you add at the index positions which you can read as the third parameter. You count from left to right in a row-wise manner.
I hope that helped :)
Matplotlib provides several ways deal with the deliberate placement of plots on a single page; i think the best is gridspec, which i believe first appeared in the 1.0 release. The other two, by the way, are (i) directly indexing subplot and (ii) the new ImageGrid toolkit).
GridSpec works like grid-based packers in GUI toolkits used to placed widgets in a parent frame, so for that reason at least, it seems the easiest to use and the most configurable of the three placement techniques.
import numpy as NP
import matplotlib.pyplot as PLT
import matplotlib.gridspec as gridspec
import matplotlib.cm as CM
V = 10 * NP.random.rand(10, 10) # some data to plot
fig = PLT.figure(1, (5., 5.)) # create the top-level container
gs = gridspec.GridSpec(4, 4) # create a GridSpec object
# for the arguments to subplot that are identical across all four subplots,
# to avoid keying them in four times, put them in a dict
# and let subplot unpack them
kx = dict(frameon = False, xticks = [], yticks = [])
ax1 = PLT.subplot(gs[0, 0], **kx)
ax3 = PLT.subplot(gs[2, 0], **kx)
ax2 = PLT.subplot(gs[1, 1], **kx)
ax4 = PLT.subplot(gs[3, 1], **kx)
for itm in [ax1, ax2, ax3, ax4] :
itm.imshow(V, cmap=CM.jet, interpolation='nearest')
PLT.show()
Beyond just arranging the four plots in a 'checkerboard' configuration (per your Question), I have not tried to tune this configuration, but that's easy to do. E.g.,
# to change the space between the cells that hold the plots:
gs1.update(left=.1, right=,1, wspace=.1, hspace=.1)
# to create a grid comprised of varying cell sizes:
gs = gridspec.GridSpec(4, 4, width_ratios=[1, 2], height_ratios=[4, 1])

Categories

Resources