Fixed ylabel space (aligned y-axis) across multiple figures - python

I'm using code much like:
import matplotlib.pyplot as plt
labels = ['AAA', 'BBBBBBBBBBBBB', 'CCCCCC', 'DDDDDDDDDD']
values = [0, 2, 2, 5]
fig, ax = plt.subplots(figsize=(8, 0.07 + 0.25 * len(values)))
bars = ax.barh(labels, values, color=colors)
to generate horizontal bar plots as separate figures, one after another:
How can I make the left spines (i.e. the black bars) align when the width of labels varies between plots? (Aside from just aligning the rendered images to the right.)
I think the left margin/padding/space should be fixed, or the bar width should be fixed, but I can't quite figure how to do it.

In these cases, I just add empty axes at the left edge of each figure. I'm sure there are more sophisticated ways, but I find this to be simplest:
fig1 with blank axes at left location
fig1, ax = plt.subplots(figsize=(8, 1))
ax.barh(['AAA', 'BBBBBBBBBBBBB', 'CCCCCC', 'DDDDDDDDDD'], [0, 2, 2, 5])
# add empty axes at `left` location (unit: fraction of figure width)
left = -0.05 # requires manual adjustment
fig1.add_axes([left, 0, 0, 0.01]).axis('off')
plt.show()
fig2 with blank axes at same left location as fig1
fig2, ax = plt.subplots(figsize=(8, 1))
ax.barh(['AAaaaA', 'BBBB', 'CCCCCC', 'DDDDD'], [2, 8, 7, 1])
# add empty axes at same `left` location as fig1
fig2.add_axes([left, 0, 0, 0.01]).axis('off')
plt.show()
Output of fig1 and fig2:
A similar approach would be to annotate a space character at the left of each figure:
ax.annotate(' ', (left, 0), xycoords='axes fraction', annotation_clip=False)

Related

How can I add text to the same position in multiple matplotlib plots with different axis scales?

I have ~20 plots with different axes, ranging from scales of 0-1 to 0-300. I want to use plt.text(x,y) to add text to the top left corner in my automated plotting function, but the changing axis size does not allow for this to be automated and completely consistent.
Here are two example plots:
import matplotlib.pyplot as plt
plt.plot([1, 2, 3, 4])
plt.ylabel('some numbers')
plt.show()
#Plot 2
plt.plot([2, 4, 6, 8])
plt.ylabel('some numbers')
plt.show()
I want to use something like plt.text(x, y, 'text', fontsize=8) in both plots, but without specifying the x and y for each plot by hand, instead just saying that the text should go in the top left. Is this possible?
Have you tried ax.text with transform=ax.transAxes?
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.plot([1, 2, 3, 4])
ax.set_ylabel('XXX')
ax.text(0.05, 0.95, 'text', transform=ax.transAxes, fontsize=8, va='top', ha='left')
plt.show()
Explanation:
The ax.text takes first the x and y coordinates of the text, then the transform argument which specifies the coordinate system to use, and the va and ha arguments which specify the vertical and horizontal alignment of the text.
Use ax.annotate with axes fraction coordinates:
fig, axs = plt.subplots(1, 2)
axs[0].plot([0, 0.8, 1, 0.5])
axs[1].plot([10, 300, 200])
for ax in axs:
ax.annotate('text', (0.05, 0.9), xycoords='axes fraction')
# ------------------------
Here (0.05, 0.9) refers to 5% and 90% of the axes lengths, regardless of the data:

How to have a common y-label between two subplots? [duplicate]

I have the following plot:
fig,ax = plt.subplots(5,2,sharex=True,sharey=True,figsize=fig_size)
and now I would like to give this plot common x-axis labels and y-axis labels. With "common", I mean that there should be one big x-axis label below the whole grid of subplots, and one big y-axis label to the right. I can't find anything about this in the documentation for plt.subplots, and my googlings suggest that I need to make a big plt.subplot(111) to start with - but how do I then put my 5*2 subplots into that using plt.subplots?
This looks like what you actually want. It applies the same approach of this answer to your specific case:
import matplotlib.pyplot as plt
fig, ax = plt.subplots(nrows=3, ncols=3, sharex=True, sharey=True, figsize=(6, 6))
fig.text(0.5, 0.04, 'common X', ha='center')
fig.text(0.04, 0.5, 'common Y', va='center', rotation='vertical')
Since I consider it relevant and elegant enough (no need to specify coordinates to place text), I copy (with a slight adaptation) an answer to another related question.
import matplotlib.pyplot as plt
fig, axes = plt.subplots(5, 2, sharex=True, sharey=True, figsize=(6,15))
# add a big axis, hide frame
fig.add_subplot(111, frameon=False)
# hide tick and tick label of the big axis
plt.tick_params(labelcolor='none', which='both', top=False, bottom=False, left=False, right=False)
plt.xlabel("common X")
plt.ylabel("common Y")
This results in the following (with matplotlib version 2.2.0):
New in Matplotlib v3.4 (pip install matplotlib --upgrade)
supxlabel and supylabel
fig.supxlabel('common_x')
fig.supylabel('common_y')
See example:
import matplotlib.pyplot as plt
for tl, cl in zip([True, False, False], [False, False, True]):
fig = plt.figure(constrained_layout=cl, tight_layout=tl)
gs = fig.add_gridspec(2, 3)
ax = dict()
ax['A'] = fig.add_subplot(gs[0, 0:2])
ax['B'] = fig.add_subplot(gs[1, 0:2])
ax['C'] = fig.add_subplot(gs[:, 2])
ax['C'].set_xlabel('Booger')
ax['B'].set_xlabel('Booger')
ax['A'].set_ylabel('Booger Y')
fig.suptitle(f'TEST: tight_layout={tl} constrained_layout={cl}')
fig.supxlabel('XLAgg')
fig.supylabel('YLAgg')
plt.show()
see more
Without sharex=True, sharey=True you get:
With it you should get it nicer:
fig, axes2d = plt.subplots(nrows=3, ncols=3,
sharex=True, sharey=True,
figsize=(6,6))
for i, row in enumerate(axes2d):
for j, cell in enumerate(row):
cell.imshow(np.random.rand(32,32))
plt.tight_layout()
But if you want to add additional labels, you should add them only to the edge plots:
fig, axes2d = plt.subplots(nrows=3, ncols=3,
sharex=True, sharey=True,
figsize=(6,6))
for i, row in enumerate(axes2d):
for j, cell in enumerate(row):
cell.imshow(np.random.rand(32,32))
if i == len(axes2d) - 1:
cell.set_xlabel("noise column: {0:d}".format(j + 1))
if j == 0:
cell.set_ylabel("noise row: {0:d}".format(i + 1))
plt.tight_layout()
Adding label for each plot would spoil it (maybe there is a way to automatically detect repeated labels, but I am not aware of one).
Since the command:
fig,ax = plt.subplots(5,2,sharex=True,sharey=True,figsize=fig_size)
you used returns a tuple consisting of the figure and a list of the axes instances, it is already sufficient to do something like (mind that I've changed fig,axto fig,axes):
fig,axes = plt.subplots(5,2,sharex=True,sharey=True,figsize=fig_size)
for ax in axes:
ax.set_xlabel('Common x-label')
ax.set_ylabel('Common y-label')
If you happen to want to change some details on a specific subplot, you can access it via axes[i] where i iterates over your subplots.
It might also be very helpful to include a
fig.tight_layout()
at the end of the file, before the plt.show(), in order to avoid overlapping labels.
It will look better if you reserve space for the common labels by making invisible labels for the subplot in the bottom left corner. It is also good to pass in the fontsize from rcParams. This way, the common labels will change size with your rc setup, and the axes will also be adjusted to leave space for the common labels.
fig_size = [8, 6]
fig, ax = plt.subplots(5, 2, sharex=True, sharey=True, figsize=fig_size)
# Reserve space for axis labels
ax[-1, 0].set_xlabel('.', color=(0, 0, 0, 0))
ax[-1, 0].set_ylabel('.', color=(0, 0, 0, 0))
# Make common axis labels
fig.text(0.5, 0.04, 'common X', va='center', ha='center', fontsize=rcParams['axes.labelsize'])
fig.text(0.04, 0.5, 'common Y', va='center', ha='center', rotation='vertical', fontsize=rcParams['axes.labelsize'])
Update:
This feature is now part of the proplot matplotlib package that I recently released on pypi. By default, when you make figures, the labels are "shared" between subplots.
Original answer:
I discovered a more robust method:
If you know the bottom and top kwargs that went into a GridSpec initialization, or you otherwise know the edges positions of your axes in Figure coordinates, you can also specify the ylabel position in Figure coordinates with some fancy "transform" magic.
For example:
import matplotlib.pyplot as plt
import matplotlib.transforms as mtransforms
bottom, top = 0.1, 0.9
fig, axs = plt.subplots(nrows=2, ncols=1, bottom=bottom, top=top)
avepos = 0.5 * (bottom + top)
transform = mtransforms.blended_transform_factory(mtransforms.IdentityTransform(), fig.transFigure) # specify x, y transform
axs[0].yaxis.label.set_transform(transform) # changed from default blend (IdentityTransform(), axs[0].transAxes)
axs[0].yaxis.label.set_position((0, avepos))
axs[0].set_ylabel('Hello, world!')
...and you should see that the label still appropriately adjusts left-right to keep from overlapping with labels, just like normal, but will also position itself exactly between the desired subplots.
Notably, if you omit the set_position call, the ylabel will show up exactly halfway up the figure. I'm guessing this is because when the label is finally drawn, matplotlib uses 0.5 for the y-coordinate without checking whether the underlying coordinate transform has changed.
I ran into a similar problem while plotting a grid of graphs. The graphs consisted of two parts (top and bottom). The y-label was supposed to be centered over both parts.
I did not want to use a solution that depends on knowing the position in the outer figure (like fig.text()), so I manipulated the y-position of the set_ylabel() function. It is usually 0.5, the middle of the plot it is added to. As the padding between the parts (hspace) in my code was zero, I could calculate the middle of the two parts relative to the upper part.
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
# Create outer and inner grid
outerGrid = gridspec.GridSpec(2, 3, width_ratios=[1,1,1], height_ratios=[1,1])
somePlot = gridspec.GridSpecFromSubplotSpec(2, 1,
subplot_spec=outerGrid[3], height_ratios=[1,3], hspace = 0)
# Add two partial plots
partA = plt.subplot(somePlot[0])
partB = plt.subplot(somePlot[1])
# No x-ticks for the upper plot
plt.setp(partA.get_xticklabels(), visible=False)
# The center is (height(top)-height(bottom))/(2*height(top))
# Simplified to 0.5 - height(bottom)/(2*height(top))
mid = 0.5-somePlot.get_height_ratios()[1]/(2.*somePlot.get_height_ratios()[0])
# Place the y-label
partA.set_ylabel('shared label', y = mid)
plt.show()
picture
Downsides:
The horizontal distance to the plot is based on the top part, the bottom ticks might extend into the label.
The formula does not take space between the parts into account.
Throws an exception when the height of the top part is 0.
There is probably a general solution that takes padding between figures into account.

Pyplot - add single text to xaxis (like a tick)

Imagine you have a pyplot with a vertical line (matplotlib.axes.Axes.axvline) at a specific location x. Now I would like to have a text like "COG" on the x-axis at x, like as if it was a tick. It can be either on the visible or non-visible axis or both.
However,
ticks already exist (given array)
shared x-axis for subplots, only lowest visible
I though about using normal text (matplotlib.pyplot.text), but
it would be inside the subplot
it would not be in xaxis relation (at least I didn't find a working way so far)
I feel like manually editing the ticks to add a single item is a not so nice workaround..
Thanks ahead!
Here is an example graph of subplots from matplotlib:
import matplotlib.pyplot as plt
import numpy as np
# Some example data to display
x = np.linspace(0, 2 * np.pi, 400)
y = np.sin(x ** 2)
fig, axs = plt.subplots(2, sharex=True)
fig.suptitle('Vertically stacked subplots')
axs[0].plot(x, y)
axs[1].plot(x, -y)
To add the elements you wanted, you can just use axvline and text; Text elements can be outside of the boundaries of the graph (and in fact the tick labels are Text).
#continued from above:
axs[0].xaxis.set_visible(False)
axs[0].axvline(4.5, color='red')
axs[0].text(4.5, -.05, 'COG', color='red', transform=axs[0].get_xaxis_transform(),
ha='center', va='top')
axs[1].axvline(4.5, color='red')
axs[1].text(4.5, -.05, 'COG', color='red', transform=axs[1].get_xaxis_transform(),
ha='center', va='top')
You can instead add another tick and change its color:
#again, continued from the first code block
axs[0].xaxis.set_visible(False)
axs[0].axvline(4.5, color='red')
axs[0].text(4.5, -.05, 'COG', color='red', transform=axs[0].get_xaxis_transform(),
ha='center', va='top')
ticks = [0, 1, 2, 3, 4, 4.5, 5, 6]
labels = [0, 1, 2, 3, 4, "COG", 5, 6]
axs[1].axvline(4.5, color='red')
axs[1].set_xticks(ticks)
axs[1].set_xticklabels(labels)
axs[1].get_xticklabels()[5].set_color('red')
But, if you don't want ticks on the top graph, then it seems like adding Text (as in the first example) is simplest. Additionally, manually setting the ticks in the second example seems more verbose, and there's the issue of selecting the tick you want to change (I index with axs[1].get_xticklabels()[5] here, but with more ticks/nastier numbers you might need something smarter). So I prefer the first approach better than this, but it might be useful in some cases (like if you want your line to occur on an existing tick).
Using Toms´s first example leads to the desired outcome.
Additionally, for the case of an overlapping text on the label, I searched for neighboring tick labels and set their transparency != 1. Thus, the text "cog" is always visible.
import matplotlib.pyplot as plt
import numpy as np
xV = 4.5
dxV = 1/4 # best 1/4 of the spacing between two ticks
# Some example data to display
x = np.linspace(0, 2 * np.pi, 400)
y = np.sin(x ** 2)
fig, axs = plt.subplots(2, sharex=True)
fig.suptitle('Vertically stacked subplots')
axs[0].plot(x, y)
axs[0].xaxis.set_visible(False)
axs[0].axvline(xV, color='red')
axs[0].text(xV, -.05, 'COG', color='red', transform=axs[0].get_xaxis_transform(),
ha='center', va='top')
axs[1].plot(x, -y)
axs[1].axvline(xV, color='red')
axs[1].text(xV, -.05, 'COG', color='red', transform=axs[1].get_xaxis_transform(),
ha='center', va='top')
# Change Transparency if too close
xticks = axs[1].xaxis.get_major_ticks()
values = axs[1].get_xticks()
# values = axs[1].xaxis.get_major_locator()()
pos = np.where(np.logical_and( (xV-dxV) <= values, (xV+dxV) >= values))[0]
if pos.size > 0:
dist = np.abs(values[pos]-xV)
pos = pos[dist.argmin()]
xticks[pos].label1.set_alpha(0.5)
plt.show()

Matplotlib: How to add a line on the colorbar of an imshow()

Basically, I found these instructions on how to add a custom line or point on a colorbar, but nothing happens with my code.
I tried changing the zorder, or switching to the pyplot style, but still couldn't get it to work.
fig, ax = plt.subplots()
im = ax.imshow(data, cmap='rainbow')
cbar = ax.figure.colorbar(im)
cbar.outline.set_visible(False)
cbar.ax.set_ylabel("α", rotation=0, va="bottom")
# None of them does anything
cbar.ax.plot(0.5, 0.5, 'k.', label="Ref")
cbar.ax.plot(0.5, 3, 'k.', label="Ref")
cbar.ax.plot([0, 1], [3, 3], 'k', label="Ref")
The figure so far looks like this
I am using matplotlib 3.1.1
Matplotlib will set the x and y limits of the colorbar to the same values. So if the y limits of your color bar are 1.8 and 7.2, the x limits are as well. You will need to plot within these limits to see your markers.
Using axhline might be an easier option instead of plotting a line with plot. To label this marked value, you can add it to the colorbar ticks and ticklabels.
# clim = cbar.ax.get_ylim() # find axis limits
# cbar.ax.plot(mean(clim), 3, 'k.') # point at 3 in the middle of x axis
cbar.ax.axhline(y=3, c='w') # line at 3
# Set label
original_ticks = list(cbar.get_ticks())
cbar.set_ticks(original_ticks + [3])
cbar.set_ticklabels(original_ticks + ['Ref'])
You probably want annotate instead of plot in this case. The label you specified in plot call won't show up until a legend() call, and even in that case the legend will not show up where you want them to be.
fig, ax = plt.subplots()
im = ax.imshow(np.random.normal(size=100).reshape(10, 10), cmap='rainbow')
cbar = ax.figure.colorbar(im)
cbar.outline.set_visible(False)
cbar.ax.set_ylabel("α", rotation=0, va="bottom")
cbar.ax.annotate('Ref', (0, 0))

Python/Matplotlib: controlling the aspect ratio in gridspec

I am trying to control the aspect ratio of 4 x 1 grid using GridSpec. I want very wide plots in the horizontal direction, and compact in the vertical direction, but I can't change the aspect ratio predictably (see first pic). All three settings of the 'height_ratios' below are giving me the same aspect ratio (see second pic below):
One:
gs = gridspec.GridSpec(4, 1, width_ratios=[1], height_ratios=[0.1, 0.1, 0.1, 0.1])
gs.update(wspace = 0, hspace = 0)
ax1 = plt.subplot(gs[0])
ax2 = plt.subplot(gs[1])
ax3 = plt.subplot(gs[2])
ax4 = plt.subplot(gs[3])
plt.show()
Two:
gs = gridspec.GridSpec(4, 1, width_ratios=[1], height_ratios=[1, 1, 1, 1])
Three:
gs = gridspec.GridSpec(4, 1, width_ratios=[1], height_ratios=[0.5, 0.5, 0.5, 0.5])
I was able to get elongated plots, like I want, by doing this:
ax1.set_aspect(0.1)
ax2.set_aspect(0.1)
ax3.set_aspect(0.1)
ax4.set_aspect(0.1)
But this adds space between the subplots, which I don't want, and I removed using hspace = 0. How do I control the aspect ratio without adding space between the subplots?
This is what I want, but I can't seem to get it again and I'm not sure why:
Instead, all I get is this:
This is what I get using ax.set_aspect(0.1), which has the correct aspect ratio, but it introduces space between the plots, which I don't want:
You need to set the figure size so you'll get the aspect ratio you want, which is controlled through the figsize parameter of plt.figure. It's not clear that you actually need GridSpec in this example; I would do:
import matplotlib.pyplot as plt
f, axes = plt.subplots(4, 1, figsize=(10, 4))
f.subplots_adjust(hspace=0)
If you do need to use GridSpec for a more complicated layout, you can make the figure with plt.figure and then pass the grid slices to the add_subplot method of the object that is returned.

Categories