pythonmatplotlibseabornlegend

Customizing legend in Seaborn histplot subplots


I am trying to generate a figure with 4 subplots, each of which is a Seaborn histplot. The figure definition lines are:

fig,axes=plt.subplots(2,2,figsize=(6.3,7),sharex=True,sharey=True)
(ax1,ax2),(ax3,ax4)=axes
fig.subplots_adjust(wspace=0.1,hspace=0.2)

I would like to define strings for legend entries in each of the subplots. As an example, I am using the following code for the first subplot:

sp1=sns.histplot(df_dn,x="ktau",hue="statind",element="step", stat="density",common_norm=True,fill=False,palette=colvec,ax=ax1)
ax1.set_title(r'$d_n$')
ax1.set_xlabel(r'max($F_{a,max}$)')
ax1.set_ylabel(r'$\tau_{ken}$')
legend_labels,_=ax1.get_legend_handles_labels()
ax1.legend(legend_labels,['dep-','ind-','ind+','dep+'],title='Stat.ind.')

The legend is not showing correctly (legend entries are not plotted and the legend title is the name of the hue variable ("statind"). Please note I have successfully used the same code for other figures in which I used Seaborn relplots instead of histplots.


Solution

  • The main problem is that ax1.get_legend_handles_labels() returns empty lists (note that the first return value are the handles, the second would be the labels).

    Basically, there are two ways to change the legend labels. One is to reuse the "handles" of the existing legend. Another approach is to rename the values.

    Creating a new legend using the old legend handles

    To get the handles, you can do

    legend = ax1.get_legend()
    handles = legend.legend_handles
    

    (In older matplotlib versions, the name was legend.legendHandles)

    A new legend can be created starting from some handles of the existing legend. Creating a new legend automatically removes an existing one.

    Also note that to be sure of the order of the labels, it helps to set hue_order. Here is some example code to show the ideas:

    import matplotlib.pyplot as plt
    import numpy as np
    import pandas as pd
    import seaborn as sns
    
    df_dn = pd.DataFrame({'ktau': np.random.randn(4000).cumsum(),
                          'statind': np.repeat([*'abcd'], 1000)})
    
    fig, ax1 = plt.subplots()
    sns.histplot(df_dn, x="ktau", hue="statind", hue_order=['a', 'b', 'c', 'd'],
                 element="step", stat="density", common_norm=True, fill=False, ax=ax1)
    ax1.set_title(r'$d_n$')
    ax1.set_xlabel(r'max($F_{a,max}$)')
    ax1.set_ylabel(r'$\tau_{ken}$')
    legend = ax1.get_legend()
    handles = legend.legend_handles
    ax1.legend(handles, ['dep-', 'ind-', 'ind+', 'dep+'], title='Stat.ind.')
    plt.show()
    

    example plot

    Temporarily renaming the values in the dataframe

    Renaming the column values, will put these in the legend. The title can either be changed by changing the column name, or by using sns.movelegend(..., title=...). Its name may be a bit misleading, as sns.movelegend() not only serves to move the legend's location, but also other properties (font size, number of columns, ...).

    fig, ax1 = plt.subplots()
    df_renamed = df_dn.replace({'statind': {'a': 'dep-', 'b': 'ind-', 'c': 'ind+', 'd': 'dep+'}})
    
    sns.histplot(df_renamed, x="ktau", hue="statind",
                 element="step", stat="density", common_norm=True, fill=False, ax=ax1)
    ax1.set_title(r'$d_n$')
    ax1.set_xlabel(r'max($F_{a,max}$)')
    ax1.set_ylabel(r'$\tau_{ken}$')
    sns.move_legend(ax1, loc='best', title='Stat.ind.')
    plt.show()