Python subplots leaving space for common axis labels - python

I have the following code that makes four subplots in one figure:
f = figure( figsize=(7,7) )
f.add_axes([0.2,0.175,0.75,0.75])
f.subplots_adjust(left=0.15)
f.clf()
ax = f.add_subplot(111)
ax1 = f.add_subplot(221)
ax2 = f.add_subplot(222)
ax3 = f.add_subplot(223)
ax4 = f.add_subplot(224)
ax.xaxis.set_major_formatter( NullFormatter() )
ax.yaxis.set_major_formatter( NullFormatter() )
ax2.xaxis.set_major_formatter( NullFormatter() )
ax2.yaxis.set_major_formatter( NullFormatter() )
ax1.xaxis.set_major_formatter( NullFormatter() )
ax4.yaxis.set_major_formatter( NullFormatter() )
f.subplots_adjust(wspace=0,hspace=0)
ax1.plot(tbins[0:24], mean_yszth1, color='r', label='mean', marker='.', lw=3)
ax2.plot(tbins[0:24], mean_ysz1, color='r', label='mean', marker='.', lw=3)
ax3.plot(tbins[0:24], mean_yszth2, color='r', label='mean', marker='.', lw=3)
ax4.plot(tbins[0:24], mean_ysz2, color='r', label='mean', marker='.', lw=3)
ax1.set_xlim(0,12)
ax1.set_ylim(-0.5,0.5)
ax2.set_xlim(0,12)
ax2.set_ylim(-0.5,0.5)
ax3.set_xlim(0,12)
ax3.set_ylim(-0.5,0.5)
ax4.set_xlim(0,12)
ax4.set_ylim(-0.5,0.5)
ax.set_xlabel(r"$\mathrm{Time\ since\ last\ merger\ (Gyr)}$")
ax.set_ylabel(r"$\mathrm{\Delta Y_{SZ}/Y_{SZ}}$")
The result looks like this:
As you can see, the axis labels overlap with the ticks. I would like to move the common axis labels away from the axes a little. I can't figure out how best to do this.

Use labelpad parameter of set_ylabel and set_xlabel methods:
Definition: ax.set_ylabel(self, ylabel, fontdict=None, labelpad=None, **kwargs)
Docstring:
Call signature::
set_ylabel(ylabel, fontdict=None, labelpad=None, **kwargs)
Set the label for the yaxis
*labelpad* is the spacing in points between the label and the y-axis
This is what I get with labelpad set to 50 (x) and 60 (y). I had to modify manually figure margins as the labels were outside the figure frame when using the default configuration.
Edit
From your comments it seems you could be using a very old version of matplotlib. Labelpad parameter has been in matplotlib from many versions ago but the way to of setting it could be different (I do not know for sure).
In the web I found some comments that point to this usage:
ax.xaxis.LABELPAD = 8 # default is 5
also I have seen it like:
ax.xaxis.labelpad = 8

Related

Trying to plot 2 charts side-by-side, but one of them always comes out empty

I have two plots that I generated from my data:
Here the second plot shows the distribution of results from the first one.
What I want is to plot them side-by-side so you could see both the data and the distribution on the same plot. And I want plots to share y-axis as well.
I tried to do the following:
fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(40, 15), sharey=True)
ax1 = sns.lineplot(plotting_df.index, plotting_df.error, color=('#e65400'), lw=2, label='random forest residual error')
ax1 = sns.lineplot(plotting_df.index, plotting_df.val, color=('#9b9b9b'), lw=1, label='current model residual error')
ax1 = sns.lineplot(plotting_df.index, 0, color=('#2293e3'), lw=1)
ax1.xaxis.set_visible(False)
ax1.set_ylabel('Residual Fe bias', fontsize=16)
ax1.set_title('Models residual error comparison', fontsize=20, fontweight='bold')
sns.despine(ax=ax1, top=True, bottom=True, right=True)
ax2 = sns.distplot(results_df.error, hist=True, color=('#e65400'), bins=81,
label='Random forest model', vertical=True)
ax2 = sns.distplot(plotting_df.val, hist=True, color=('#9b9b9b'),
bins=81, label='Rolling averages model', vertical=True)
ax2.set_title('Error distribution comparison between models', fontsize=20, fontweight='bold')
sns.despine(ax=ax2, top=True, right=True)
fig.savefig("blabla.png", format='png')
But when I do run it I get strange results - the first chart is in the second column, whereas I wanted it on the left and the second chart is completely blank. Not sure what I did wrong here.
Both lineplot and distplot accept a matplotlib axes object as an argument, which tells it which axes to plot onto. If no axes is passed into it, then the plot is placed onto the current axes.
You create a figure and 2 axes using :
fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(40, 15), sharey=True)
Therefore, ax2 will be the current axes. So your distplot is being plotted on top of your lineplot, both in ax2.
You need to pass the axes into the seaborn plotting functions.
sns.lineplot(..., ax=ax1)
sns.distplot(..., ax=ax2)

Matplotlib/Seaborn: ValueError when annotating 2nd subplot (annotating 1st subplot was successful)

I created this figure with two subplots and wanted to annotate both subplots separately (see figure and code below)
# Declare fig ax
fig, ax = plt.subplots(2,1,figsize=[12,10], sharey=True)
# Line plot for ax[0]
sns.lineplot(x=['2020-01-01', '2020-01-31'], y=[32.7477, 49.6184], ax=ax[0], marker='o', markersize=10)
# Add title for ax[0]
ax[0].set(title='Figure 1', xlabel=None, ylabel="Index")
ax[0].tick_params(axis = 'x', rotation = 45)
# Annotation for ax[0]
ax[0].text(0, 55, 52.53)
ax[0].text(0.4, 45, "Some annotation here", horizontalalignment='center', size='medium', color='black')
ax[0].text(1, 52, 49.62, horizontalalignment='center', size='medium', color='black')
# Line plot for ax[1]
sns.lineplot(x="date", y="ari", data=df_ari_jan, ax=ax[1], marker='o', markersize=10)
# Add title for ax[1]
ax[1].set(title='Figure 2', xlabel=None, ylabel="Index")
ax[1].set_xticks(df_ari_jan["date"].values)
ax[1].tick_params(axis = 'x', rotation = 45)
# Annotation for ax[1]
ax[1].text(-1, 0, 52.53)
fig.tight_layout()
Annotating the first subplot was fine, but I kept getting this ValueError: Image size of 15006958x630 pixels is too large. It must be less than 2^16 in each direction. when trying to annotate the 2nd one.
fig.tight_layout() wouldn't work either UserWarning: Tight layout not applied. The left and right margins cannot be made large enough to accommodate all axes decorations.
Any idea as to why?

No handles with labels found to put in legend

I'm trying to create a parallelogram in PyPlot. I'm not up to drawing the parallelogram--first I'm putting in the vector arrows--using the following code:
fig = plt.figure()
ax = fig.add_subplot(111)
ax.spines['left'].set_position('zero')
ax.spines['right'].set_color('none')
ax.spines['bottom'].set_position('zero')
ax.spines['top'].set_color('none')
plt.axis([-5,5,-5,5])
ax.xaxis.set_ticks_position('bottom')
ax.yaxis.set_ticks_position('left')
plt.grid()
plt.arrow(0,0, 3,1, head_width=0.2, color='r', length_includes_head=True, label='u')
plt.arrow(0,0, 1,3, head_width=0.2, color='r', length_includes_head=True, label='v')
plt.arrow(0,0, 4,4, head_width=0.2, color='r', length_includes_head=True, label='u+v')
plt.legend()
This returns the following error:
No handles with labels found to put in legend.
I'm not sure why, because, based on the documentation for plt.arrow(), label is an acceptable kwarg, and plt.legend() should ostensibly be reading that. The rest of the figure draws fine; it's just missing the legend.
It might be late but for anyone with the same issue the solution is using the method legend() for the corresponding ax not as for plt
fig = plt.figure()
ax = fig.add_subplot(111)
ax.spines['left'].set_position('zero')
ax.spines['right'].set_color('none')
ax.spines['bottom'].set_position('zero')
ax.spines['top'].set_color('none')
plt.axis([-5,5,-5,5])
ax.xaxis.set_ticks_position('bottom')
ax.yaxis.set_ticks_position('left')
plt.grid()
plt.arrow(0,0, 3,1, head_width=0.2, color='r', length_includes_head=True, label='u')
plt.arrow(0,0, 1,3, head_width=0.2, color='r', length_includes_head=True, label='v')
plt.arrow(0,0, 4,4, head_width=0.2, color='r', length_includes_head=True, label='u+v')
ax.legend()
You can explicitly define the elements in the legend.
For full control of which artists have a legend entry, it is possible to pass an iterable of legend artists followed by an iterable of legend labels respectively. Reference
Example:
arr1 = plt.arrow(0,0, 3,1, head_width=0.2, color='r', length_includes_head=True)
arr2 = plt.arrow(0,0, 1,3, head_width=0.2, color='g', length_includes_head=True)
arr3 = plt.arrow(0,0, 4,4, head_width=0.2, color='b', length_includes_head=True)
plt.xlim(0,5)
plt.ylim(0,5)
plt.legend([arr1, arr2, arr3], ['u','v','u+v'])
The error is thrown because you haven't specified the label text
Either do something like this
plt.hist([x01, x02,x03], color=["lightcoral","lightskyblue","slategrey"], stacked=True,
label=['Supressed','Active','Resolved'])
plt.legend()
Or
Do not use plt.legend() if you haven't specified the label text as in the following WRONG example:
plt.hist([x01])
plt.legend()
The above will throw the same error, so either remove legend function or provide what it needs -> label.
Side note: Here x01 is just a list of number for which I am creating a histogram, in the first example they are three list of numbers to create stacked bar chart
The bottom line is this error is thrown because of not specifying legend text and calling/initializing a legend
I had this error when using labels which started with an underscore
plt.plot(x, y, label = '_bad_name')
Removing the front underscore from the labels solved the issue
Assuming you have 2 plots ax and ax2, we can:
get the labels from each y-axis via ax.get_label()
.legend allows an array to be ingested
fig.legend([ax.get_ylabel(), ax2.get_ylabel()], loc='upper right')
I had this same issue and solved it with an understanding that .legend() function has to be after all the instructions that deal with the label attribute. This includes both plt and ax.
So moving ax.legend(*) as the last command.
I hope this helps you too.
Change
ax.plot(-trip_df.stop_lat, -trip_df.stop_lon, label = trip_id)
plt.legend()
to
ax.plot(-trip_df.stop_lat, -trip_df.stop_lon, label = trip_id)
ax.legend()
plt.legend()
I face similar problem like No handles with labels found to put in legend.
First My code look like
figure, axis = pyplot.subplots(nrows=1,ncols=2, figsize=(15, 6), tight_layout=True)
axis[0].legend(title='Country', title_fontsize = 12) #this line
axis[0].pie(x=piechart_result['value_eur'],labels=piechart_result['short_name'])
axis[1].pie(x=piechart_result['value_eur'],labels=piechart_result['short_name')
pyplot.show()
Then I changed to
figure, axis = pyplot.subplots(nrows=1,ncols=2, figsize=(15, 6), tight_layout=True)
axis[0].pie(x=piechart_result['value_eur'],labels=piechart_result['short_name'])
axis[0].legend(title='Country', title_fontsize = 12) # this line
axis[1].pie(x=piechart_result['value_eur'],labels=piechart_result['short_name')
pyplot.show()
this work for me in colab notebook

Plot in subplot matplotlib with custom (external) function

I have a similar question to what has been answered before (matplotlib: make plots in functions and then add each to a single subplot figure). However, I want to have more advanced plots. I'm using this function for plotting (taken from https://towardsdatascience.com/an-introduction-to-bayesian-inference-in-pystan-c27078e58d53):
def plot_trace(param, param_name='parameter', ax=None, **kwargs):
"""Plot the trace and posterior of a parameter."""
# Summary statistics
mean = np.mean(param)
median = np.median(param)
cred_min, cred_max = np.percentile(param, 2.5), np.percentile(param, 97.5)
# Plotting
#ax = ax or plt.gca()
plt.subplot(2,1,1)
plt.plot(param)
plt.xlabel('samples')
plt.ylabel(param_name)
plt.axhline(mean, color='r', lw=2, linestyle='--')
plt.axhline(median, color='c', lw=2, linestyle='--')
plt.axhline(cred_min, linestyle=':', color='k', alpha=0.2)
plt.axhline(cred_max, linestyle=':', color='k', alpha=0.2)
plt.title('Trace and Posterior Distribution for {}'.format(param_name))
plt.subplot(2,1,2)
plt.hist(param, 30, density=True); sns.kdeplot(param, shade=True)
plt.xlabel(param_name)
plt.ylabel('density')
plt.axvline(mean, color='r', lw=2, linestyle='--',label='mean')
plt.axvline(median, color='c', lw=2, linestyle='--',label='median')
plt.axvline(cred_min, linestyle=':', color='k', alpha=0.2, label='95% CI')
plt.axvline(cred_max, linestyle=':', color='k', alpha=0.2)
plt.gcf().tight_layout()
plt.legend()
and I would like to have those two subplots for different parameters. If I use this code, it simply overwrites the plot and ignores the ax parameter. Would you please help me how to make it work also for more than just 2 parameters?
params = [mu_a,mu_tau]
param_names = ['A mean','tau mean']
fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(16,16))
fig.subplots_adjust(hspace=0.5)
fig.suptitle('Convergence and distribution of parameters')
plot_trace(params[0], param_name=param_names[0], ax = ax1)
plot_trace(params[1], param_name=param_names[1], ax = ax2)
The expected result would be to have those plots next to each other
How one subplot should look like (what the function makes). Thank you.
As suggested by both #Sebastian-R and #ImportanceOfBeingErnest, the problem is that you are creating the subplots inside the functions instead of using what is passed to the ax= keyword. In addition, if you want to plot on two different subplots, you need to pass two axes instances to your function
There are two ways to correct the problem:
the first solution would be to re-write your function to use the object-oriented API of matplotlib (this is the solution that I recommend, although it requires more work)
code:
def plot_trace(param, axs, param_name='parameter', **kwargs):
"""Plot the trace and posterior of a parameter."""
# Summary statistics
(...)
# Plotting
ax1 = axs[0]
ax1.plot(param)
ax1.set_xlabel('samples') # notice the difference in notation here
(...)
ax2 = axs[1]
ax2.hist(...); sns.kdeplot(..., ax=ax2)
(...)
the second solution keeps your current code, with the exception that, since the plt.xxx() functions work on the current axes, you need to make the axes that you passed as an argument the current axes:
code:
def plot_trace(param, axs, param_name='parameter', **kwargs):
"""Plot the trace and posterior of a parameter."""
# Summary statistics
(...)
# Plotting
plt.sca(axs[0])
plt.plot(param)
plt.xlabel('samples')
(...)
plt.sca(axs[1])
plt.hist(...)
(...)
Then you need to create the number of subplots that you need at the end (so if you want to plot two parameters, you'll have to create 4 subplots) and call your function as you had wanted:
params = [mu_a,mu_tau]
param_names = ['A mean','tau mean']
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(nrows=2, ncols=2, figsize=(16,16))
fig.subplots_adjust(hspace=0.5)
fig.suptitle('Convergence and distribution of parameters')
plot_trace(params[0], axs=[ax1, ax3], param_name=param_names[0])
plot_trace(params[1], axs=[ax2, ax4], param_name=param_names[1])

How to control zorder and clipping with mpl_toolkits.axes_grid1.inset_locator.mark_inset?

In a plot with inset axes, I want to mark the inset using mpl_toolkits.axes_grid1.inset_locator.mark_inset. However, I'm running into trouble controlling the zorder and clipping of the resulting lines marking the inset. The inset axes are set to zorder=4, and I'm using:
fig = plt.figure()
fig.set_tight_layout(False)
ax = fig.gca()
x = np.arange(4500.0, 10000.0)
ax.plot(x, 700-x/20.0+20*np.sin(x/8.0), label="Skylines")
from mpl_toolkits.axes_grid1.inset_locator import InsetPosition, mark_inset, inset_axes
inset_ax = fig.add_axes([0,0,1,1], zorder=4, frameon=True)
inset_ax.set_axes_locator(InsetPosition(ax, [0.1, 0.1, 0.4, 0.5]))
inset_ax.plot(x, 700-x/20.0+20*np.sin(x/8.0))
inset_ax.set_xlim(8800, 8850)
inset_ax.set_ylim(230, 285)
# inset_ax.set_ylim(100, 600)
mark_inset(ax, inset_ax, loc1=2, loc2=3, linewidth=0.7, fc="None", ec='k', alpha=0.4, clip_on=True, zorder=3)
ax.axhline(y=300, c='r', label="Test")
leg = ax.legend(ncol=1, loc='upper center', frameon=True, framealpha=1.0)
leg.set_zorder(5)
plt.show()
which, for two different cases in y limits, results in
The unwanted behaviour here is that the inset lines appear across the inset axes (while the line marked Test is placed nicely behind the inset axes), and outside of the main axes (and through the legend), respectively. I would have expected the zorder and clip_on arguments to fix this, but they don't seem to have an effect.
Case 1
zorder is evaluated on a per axis base. Since the connector lines are added to the inset axes, they will always be on top of the axes background. An option is to remove them from the inset axes and add them to the original one.
Case 2
The connectors are explicitely not clipped in the matplotlib source code, because as part of the inset axes you would not ever want them to be clipped by the inset axes.
However, if they are part of the original axes, you can set clipping to on again.
In total
ret = mark_inset(ax, inset_ax, loc1=2, loc2=3, linewidth=0.7, fc="None", ec='k', alpha=0.4)
for bc in ret[1:]:
bc.remove()
ax.add_patch(bc)
bc.set_zorder(4)
bc.set_clip_on(True)

Categories

Resources