Skip to content

Cannot adjust legend with combining objects interface with matplotlib subfigures #3884

@Auerilas

Description

@Auerilas

Hi!
I'm making a figure with three subpanels. The bottom panel is a scatterplot which needs a legend. I'm using subplot_mosaic to arrange the panels, then combining the objects interface with matplotlib to have finer control and to also build the plots. One thing I've encountered is that the legend created by the objects interface does not pass to the matplotlib axis. As a result, I can't move it using either ax.legend() or sns.move_legend():

fig, axs = plt.subplot_mosaic([['hist1', 'hist2'],
                               ['scatter1', 'scatter1']],
                              figsize=(6.5, 6),
                              height_ratios=(1, 4),
                              layout='constrained')

(
    so.Plot(x='m2', y='Plot_treat', data=full_df)
    # .add(so.Dot(alpha=0.10), so.Dodge(), so.Jitter())
    .add(so.Bar(artist_kws={'zorder': 10}), so.Est(), legend=False)
    .add(so.Range(artist_kws={'zorder': 10}), so.Est(errorbar='se'), legend=False)
    .label(y='', x='Total ANPP (g m$^{\mathregular{2}}$)', legend=False)
    .scale(y=so.Nominal(order=['Ambient', 'Drought']))
    .theme(theme)
    .on(axs['hist1'])
    .plot()
)
axs['hist1'].xaxis.set_label_position('top')
axs['hist1'].xaxis.tick_top()
axs['hist1'].set_xscale('log')
axs['hist1'].set_xticks([1, 10, 50, 100])
axs['hist1'].set_xticklabels([1, 10, 50, 100])
axs['hist1'].text(75, 0.5, '*', ha='center', va='center', size=18)


(
    so.Plot(x='m2', y='Herb_treat', data=full_df)
    # .add(so.Dot(alpha=0.10), so.Dodge(), so.Jitter())
    .add(so.Bar(artist_kws={'zorder': 10}), so.Est(), legend=False)
    .add(so.Range(artist_kws={'zorder': 10}), so.Est(errorbar='se'), legend=False)
    .label(y='', x='Total ANPP (g m$^{\mathregular{2}}$)', legend=False)
    .scale(y=so.Nominal(order=['No Consumers', 'Consumers']))
    .theme(theme)
    .on(axs['hist2'])
    .plot()
)
axs['hist2'].xaxis.set_label_position('top')
axs['hist2'].xaxis.tick_top()
axs['hist2'].yaxis.tick_right()
axs['hist2'].set_xscale('log')
axs['hist2'].set_xticks([1, 10, 50, 100])
axs['hist2'].set_xticklabels([1, 10, 50, 100])
axs['hist2'].set_yticklabels(['No\nConsumers', 'Consumers'])
axs['hist2'].text(75, 0.5, '*', ha='center', va='center', size=18)

rmod = smf.ols('np.log(m2) ~ Plot_treat*np.log(Total_N)', full_df).fit()
xpred_a = np.linspace(full_df.query('Plot_treat == "Ambient"')['Total_N'].min(),
                      full_df.query('Plot_treat == "Ambient"')['Total_N'].max(),
                      100)
xpred_d = np.linspace(full_df.query('Plot_treat == "Drought"')['Total_N'].min(),
                      full_df.query('Plot_treat == "Drought"')['Total_N'].max(),
                      100)
dfa = pd.DataFrame({'Total_N': xpred_a, 'Plot_treat': 'Ambient'})
dfb = pd.DataFrame({'Total_N': xpred_d, 'Plot_treat': 'Drought'})
preddf = pd.concat((dfa, dfb))
preds = rmod.get_prediction(preddf)
preddf = (preddf
          .assign(m2 = np.exp(preds.predicted))
          .assign(upper = np.exp(preds.predicted + preds.se))
          .assign(lower = np.exp(preds.predicted - preds.se))
          .reset_index()
          )

(
    so.Plot(data=full_df)
    .add(so.Dot(alpha=0.35), x='Total_N', y='m2', color='Plot_treat', data=full_df, legend=False)
    .add(so.Line(), x='Total_N', y='m2', color='Plot_treat', data=preddf)
    .add(so.Band(), x='Total_N', ymin='lower', ymax='upper', color='Plot_treat', data=preddf)
    .scale(color=so.Nominal(order=['Ambient', 'Drought']))
    .label(x='Total Plant Available N', y='Total ANPP (g m$^{\mathregular{2}}$)', color='')
    .theme(theme)
    .on(axs['scatter1'])
    .plot()
)
sns.move_legend(axs['scatter1'], 'lower left')

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/nate/miniforge3/lib/python3.12/site-packages/seaborn/utils.py", line 432, in move_legend
    raise ValueError(err)
ValueError: Axes(0.103889,0.0726394;0.762606x0.669495) has no legend attached.

Same here:

axs['scatter1'].legend(loc='lower left')
<stdin>:1: UserWarning: No artists with labels found to put in legend.  Note that artists whose label start with an underscore are ignored when legend() is called with no argument.
<matplotlib.legend.Legend object at 0x7c4e4ebf7b60>

Ultimately, the figure looks amazing, but I have to manually move the legend in Inkscape afterwards.

Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions