I'm trying to plot multiple things in a figure with subplots.
When I try generating a legend for the figure, it is only showing labels for the data in the very last subplot, instead of for the entire figure.
fig, axs = plt.subplots(3, 2, figsize=(16,18))
first_pops = {"M71 (Masseron)":[1., "purple"], \
"6273 (Yong)":[0.1, "yellowgreen"], \
"2419 (Carretta)":[0.0, "olive"], \
"6218 (Carretta)":[0.25, "darkcyan"],
}
elements = ["O I", "Na I", "Mg I", "Si I", "Ti I", "Ca I"]
element_labels = ["[O/Fe]", "[Na/Fe]", "[Mg/Fe]", "[Si/Fe]", "[Ti/Fe]", "[Ca/Fe]"]
for k, ax in enumerate(axs.ravel()):
element = elements[k]
element_label = element_labels[k]
for GC in first_pops.keys():
try:
plot_background_stars(glob_dict[GC]["[Fe/H]"],\
glob_dict[GC][element_label],\
label = GC, ax=ax, color = first_pops[GC][1],\
s = 140, marker="*", facecolors='none', linewidth=2)
except KeyError:
print(GC+" doesn't have "+element+" measured")
ax.set_xlabel("[Fe/H]")
ax.set_ylabel(element_label)
handles, labels = fig.gca().get_legend_handles_labels()
by_label = dict(zip(labels, handles))
fig.legend(by_label.values(), by_label.keys())
I've tried replacing "fig" with "plt" and vice versa.
I'm using the following to plot each data set:
def plot_background_stars(x, y, ax=None, **plt_kwargs):
if ax is None:
ax = plt.gca()
ax.scatter(x, y, **plt_kwargs)
return(ax)
The resulting legend has just two of the plotted colors, instead of four. Like maybe it's only pulling the labels from the last subplot? But why would it do that?
Related
I've a fig with two subplots, I want them to have the same y-scale.
fig, ax = plt.subplots(1, 2,figsize=(10,6))
ax[0].set_title('Over Global region, {} points'. Format(len(df)))
ax[0].plot(df.groupby('type')['thickness'].mean(),label='Cloud mean thickness',color='red')
ax[0].set_xlabel('Cloud Type',fontsize=13)
ax[0].set_ylabel('(in km)',fontsize=15)
#Put xticks with labels at the bottom of the plot
ax[0].set_xticks(cloudlabel,cloudtype_dict.values(),rotation=50)
lon_left, lon_right, lat_bot, lat_top = 60, 100, 0, 40
dfcopy = df.copy()
dfcopy_aoi = dfcopy[(dfcopy['lon']>=lon_left) & (dfcopy['lon']<=lon_right) & (dfcopy['lat']>=lat_bot) & (dfcopy['lat']<=lat_top)]
ax[1].set_title('Over Indian subcontinent region, {} points'.format(len(dfcopy_aoi)))
ax[1].plot(dfcopy_aoi.groupby('type')['thickness'].mean(),label='Cloud mean thickness',color='red')
ax[1].set_xlabel('Cloud Type',fontsize=13)
ax[1].set_ylabel('(in km)',fontsize=15)
ax[1].set_xticks(cloudlabel,cloudtype_dict.values(),rotation=50);
#Shared y-axis
y_lim=[0,0]
y_lim = [min(ax[0].get_ylim()[0], ax[1].get_ylim()[0]), max(ax[0].get_ylim()[1], ax[1].get_ylim()[1])]
for i in range(2):
ax[i].set_ylim(y_lim)
ax[0].legend(loc='best');
ax[1].legend(loc='best');
Probelm
I'm not getting the optimal legend placement for ax[0] even though I'm putting the legend argument at the end of everything.
Below is the image I am getting with bad legend loc for the first axis.
I'm sure that I've done all things right but in the end the result I got is a sccatter plot that only shows the second datasets data.
fig = plt.figure()
ax1 = fig.add_subplot(111)
ax1.scatter(train["ENGINESIZE"], train["CO2EMISSIONS"], color = "green")
ax1.scatter(test["ENGINESIZE"], test["CO2EMISSIONS"], color = "red")
plt.xlabel("Engine Size")
plt.ylabel("Emission")
plt.show()
Here You can see what's going on in my output in link below.
It shows only red data(test data) in the output.
Where is the "output link below", please? For now I can only imagine what you are describing.
Also it helps if both plots have the same axis. That is, both have the same x-axis and then they can vary on their y-axis.
If so:
fig, ax = plt.subplots()
df.plot(kind = 'scatter', x= train["ENGINESIZE"], y = train["CO2EMISSIONS"], color = {'g'}, ax = ax)
df.plot(kind = 'scatter', x= test["ENGINESIZE"], y = test["CO2EMISSIONS"], color = {'r'}, ax = ax)
plt.xlabel()
A quite basic question about ticks' labels for x and y-axis. According to this code
fig, axes = plt.subplots(6,12, figsize=(50, 24), constrained_layout=True, sharex=True , sharey=True)
fig.subplots_adjust(hspace = .5, wspace=.5)
custom_xlim = (-1, 1)
custom_ylim = (-0.2,0.2)
for i in range(72):
x_data = ctheta[i]
y_data = phi[i]
y_err = err_phi[i]
ax = fig.add_subplot(6, 12, i+1)
ax.plot(x_data_new, bspl(x_data_new))
ax.axis('off')
ax.errorbar(x_data,y_data, yerr=y_err, fmt="o")
ax.set_xlim(custom_xlim)
ax.set_ylim(custom_ylim)
I get the following output:
With y labels for plots on the first column and x labels for theone along the last line, although I call them off.
Any idea?
As #BigBen wrote in their comment, your issue is caused by you adding axes to your figure twice, once via fig, axes = plt.subplots() and then once again within your loop via fig.add_subplot(). As a result, the first set of axes is still visible even after you applied .axis('off') to the second set.
Instead of the latter, you could change your loop to:
for i in range(6):
for j in range(12):
ax = axes[i,j] # these are the axes created via plt.subplots(6,12,...)
ax.axis('off')
# … your other code here
Inside a loop I am calculating some things and then I want to plot them in two different figures. I have set up the figures as
susc_comp, (ax1,ax2) = plt.subplots( 2, 1, sharex=True, sharey='none', figsize=(8.3,11.7))
cole_cole, (ax3) = plt.subplots( 1, 1, sharex='none', sharey='none', figsize=(8.3,11.7))
for j,temp in enumerate(indexes_T[i]):
Calculate and plot in the corresponding ax1,ax2,ax3
plt.legend(loc=0, fontsize='small', numpoints = 1, ncol=(len(indexes_T[i]))/2, frameon=False)
susc_comp.savefig('suscp_components'+str(field)+'Oe.png', dpi=300)
cole_cole.savefig('Cole_Cole'+str(field)+'Oe.png', dpi=300)
But I get the legend only in the sus_comp figure (it is the same legend for both figures). How can I select the figure and add the legend to each of them?
Thank you very much!
You can call figure.legend directly (although I think this may have less functionality than plt.legend). Therefore, I would do this a different way.
The question states that both legends are the same. In addition, the second figure only has 1 axes in it. Therefore one solution would be to get the handles and labels from ax3, then manually apply those to both figures. A simplified example is below:
import matplotlib.pyplot as plt
susc_comp, (ax1, ax2) = plt.subplots(1,2)
cole_cole, ax3 = plt.subplots()
ax1.plot([1,2,3], label="Test1")
ax2.plot([3,2,1], label="Test2")
ax3.plot([1,2,3], label="Test1")
ax3.plot([3,2,1], label="Test2")
handles, labels = ax3.get_legend_handles_labels()
ax2.legend(handles, labels, loc=1, fontsize='small', numpoints = 1)
ax3.legend(handles, labels, loc=1, fontsize='small', numpoints = 1)
plt.show()
This gives the following 2 figures:
I am trying to make some figures for a scientific article, so I want my figures to have a specific size. I also see that Matplotlib by default adds a lot of padding on the border of the figures, which I don't need (since the figures will be on a white background anyway).
To set a specific figure size I simply use plt.figure(figsize = [w, h]), and I add the argument tight_layout = {'pad': 0} to remove the padding. This works perfectly, and even works if I add a title, y/x-labels etc. Example:
fig = plt.figure(
figsize = [3,2],
tight_layout = {'pad': 0}
)
ax = fig.add_subplot(111)
plt.title('title')
ax.set_ylabel('y label')
ax.set_xlabel('x label')
plt.savefig('figure01.pdf')
This creates a pdf file with exact size 3x2 (inches).
The issue I have is that when I for example add a text box outside the axis (typically a legend box), Matplotlib does not make room for the text box like it does when adding titles/axis labels. Typically the text box is cut off, or does not show in the saved figure at all. Example:
plt.close('all')
fig = plt.figure(
figsize = [3,2],
tight_layout = {'pad': 0}
)
ax = fig.add_subplot(111)
plt.title('title')
ax.set_ylabel('y label')
ax.set_xlabel('x label')
t = ax.text(0.7, 1.1, 'my text here', bbox = dict(boxstyle = 'round'))
plt.savefig('figure02.pdf')
A solution I found elsewhere on SO was to add the argument bbox_inches = 'tight' to the savefig command. The text box is now included like I wanted, but the pdf is now the wrong size. It seems like Matplotlib just makes the figure bigger, instead of reducing the size of the axes like it does when adding titles and x/y-labels.
Example:
plt.close('all')
fig = plt.figure(
figsize = [3,2],
tight_layout = {'pad': 0}
)
ax = fig.add_subplot(111)
plt.title('title')
ax.set_ylabel('y label')
ax.set_xlabel('x label')
t = ax.text(0.7, 1.1, 'my text here', bbox = dict(boxstyle = 'round'))
plt.savefig('figure03.pdf', bbox_inches = 'tight')
(This figure is 3.307x2.248)
Is there any solution to this that covers most cases with a legend just outside the axes?
So the requirements are:
Having a fixed, predefined figure size
Adding a text label or legend outside the axes
Axes and text cannot overlap
The axes, together with the title and axis labels, sits tightly agains the figure border.
So tight_layout with pad = 0, solves 1. and 4. but contradicts 2.
One could think on setting pad to a larger value. This would solve 2. However, since it's is symmetric in all directions, it would contradict 4.
Using bbox_inches = 'tight' changes the figure size. Contradicts 1.
So I think there is no generic solution to this problem.
Something I can come up with is the following: It sets the text in figure coordinates and then resizes the axes either in horizontal or in vertical direction such that there is no overlap between the axes and the text.
import matplotlib.pyplot as plt
import matplotlib.transforms
fig = plt.figure(figsize = [3,2])
ax = fig.add_subplot(111)
plt.title('title')
ax.set_ylabel('y label')
ax.set_xlabel('x label')
def text_legend(ax, x0, y0, text, direction = "v", padpoints = 3, margin=1.,**kwargs):
ha = kwargs.pop("ha", "right")
va = kwargs.pop("va", "top")
t = ax.figure.text(x0, y0, text, ha=ha, va=va, **kwargs)
otrans = ax.figure.transFigure
plt.tight_layout(pad=0)
ax.figure.canvas.draw()
plt.tight_layout(pad=0)
offs = t._bbox_patch.get_boxstyle().pad * t.get_size() + margin # adding 1pt
trans = otrans + \
matplotlib.transforms.ScaledTranslation(-offs/72.,-offs/72.,fig.dpi_scale_trans)
t.set_transform(trans)
ax.figure.canvas.draw()
ppar = [0,-padpoints/72.] if direction == "v" else [-padpoints/72.,0]
trans2 = matplotlib.transforms.ScaledTranslation(ppar[0],ppar[1],fig.dpi_scale_trans) + \
ax.figure.transFigure.inverted()
tbox = trans2.transform(t._bbox_patch.get_window_extent())
bbox = ax.get_position()
if direction=="v":
ax.set_position([bbox.x0, bbox.y0,bbox.width, tbox[0][1]-bbox.y0])
else:
ax.set_position([bbox.x0, bbox.y0,tbox[0][0]-bbox.x0, bbox.height])
# case 1: place text label at top right corner of figure (1,1). Adjust axes height.
#text_legend(ax, 1,1, 'my text here', bbox = dict(boxstyle = 'round'), )
# case 2: place text left of axes, (1, y), direction=="v"
text_legend(ax, 1., 0.8, 'my text here', margin=2., direction="h", bbox = dict(boxstyle = 'round') )
plt.savefig(__file__+'.pdf')
plt.show()
case 1 (left) and case 2 (right):
Doin the same with a legend is slightly easier, because we can directly use the bbox_to_anchor argument and don't need to control the fancy box around the legend.
import matplotlib.pyplot as plt
import matplotlib.transforms
fig = plt.figure(figsize = [3.5,2])
ax = fig.add_subplot(111)
ax.set_title('title')
ax.set_ylabel('y label')
ax.set_xlabel('x label')
ax.plot([1,2,3], marker="o", label="quantity 1")
ax.plot([2,1.7,1.2], marker="s", label="quantity 2")
def legend(ax, x0=1,y0=1, direction = "v", padpoints = 3,**kwargs):
otrans = ax.figure.transFigure
t = ax.legend(bbox_to_anchor=(x0,y0), loc=1, bbox_transform=otrans,**kwargs)
plt.tight_layout(pad=0)
ax.figure.canvas.draw()
plt.tight_layout(pad=0)
ppar = [0,-padpoints/72.] if direction == "v" else [-padpoints/72.,0]
trans2=matplotlib.transforms.ScaledTranslation(ppar[0],ppar[1],fig.dpi_scale_trans)+\
ax.figure.transFigure.inverted()
tbox = t.get_window_extent().transformed(trans2 )
bbox = ax.get_position()
if direction=="v":
ax.set_position([bbox.x0, bbox.y0,bbox.width, tbox.y0-bbox.y0])
else:
ax.set_position([bbox.x0, bbox.y0,tbox.x0-bbox.x0, bbox.height])
# case 1: place text label at top right corner of figure (1,1). Adjust axes height.
#legend(ax, borderaxespad=0)
# case 2: place text left of axes, (1, y), direction=="h"
legend(ax,y0=0.8, direction="h", borderaxespad=0.2)
plt.savefig(__file__+'.pdf')
plt.show()
Why 72? The 72 is the number of points per inch (ppi). This is a fixed typographic unit e.g. fontsizes are always given in points (like 12pt). Because matplotlib defines the padding of the text box in units relative to fontsize, which is points, we need to use 72 to transform back to inches (and then to display coordinates). The default dots per inch (dpi) is not touched here, but is accounted for in fig.dpi_scale_trans. If you want to change dpi you need to make sure the figure dpi is set when creating the figure as well as when saving it (use dpi=.. in the call to plt.figure() as well as plt.savefig()).
As of matplotlib==3.1.3, you can use constrained_layout=True to achieve the desired result. This is currently experimental, but see the docs for a very helpful guide (and the section specifically on legends). Note that the legend will steal space from the plot, but this is unavoidable. I've found that as long as the legend does not take up too much space relative to the size of the plot, then the figure gets saved without cropping anything.
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(3, 2), constrained_layout=True)
ax.set_title('title')
ax.set_ylabel('y label')
ax.set_xlabel('x label')
ax.plot([0,1], [0,1], label='my text here')
ax.legend(loc='center left', bbox_to_anchor=(1.1, 0.5))
fig.savefig('figure03.pdf')