Search code examples
pythonpandasseaborn

Add legend in Seaborn combo line bar chart


I'm trying to add a legend to my seaborn bar + line chart, but only getting the error "No handles with labels found to put in legend." whatever I try. How to go about this?

from pathlib import Path
import pandas as pd
import seaborn as sns
from matplotlib import pyplot as plt
import matplotlib.dates as mdates
import numpy as np    

dfGroup = pd.DataFrame({
        'Year': [1910, 1911, 1912, 1913, 1914, 1915, 1916, 1917, 1918, 1919, 1920],
        'Total Deaths': [0, 0, 2, 3, 2, 3, 4, 5, 6, 7, 8],
        'Total Affected': [0, 1, 0, 2, 3, 6, 9, 8, 12, 13, 15]
        })
# Add 3-year rolling average
dfGroup['rolling_3years'] = dfGroup['Total Deaths'].rolling(3).mean().shift(0)
dfGroup = dfGroup.fillna(0)

# Make a smooth line from the 3-year rolling average
from scipy.interpolate import make_interp_spline
X_Y_Spline = make_interp_spline(dfGroup['Year'], dfGroup['rolling_3years'])
 
# Returns evenly spaced numbers over a specified interval.
X_ = np.linspace(dfGroup['Year'].min(), dfGroup['Year'].max(), 500)
Y_ = X_Y_Spline(X_)

# Plot the data
a4_dims = (15, 10)
fig, ax1 = plt.subplots(figsize=a4_dims)

ax1 = sns.barplot(x = "Year", y = "Total Deaths",
             data = dfGroup, color='#42b7bd')
ax2 = ax1.twinx()
ax2 = sns.lineplot(X_, Y_, marker='o')

Solution

  • As seaborn's barplot uses categorical positions, internally numbered 0,1,2,... both plots can be drawn on the same ax. This can be accomplished by recalculating the x values for the lineplot.

    To obtain a legend, the label= keyword should be used. (Creating a legend on twinx axes is a bit more complicated and would involve creating custom handles.) Seaborn often automatically creates a legend. If you want to change its appearance, you can call ax1.legend(...) with customization parameters.

    Here is some example code:

    from pathlib import Path
    import pandas as pd
    import seaborn as sns
    from matplotlib import pyplot as plt
    import matplotlib.dates as mdates
    import numpy as np
    
    dfGroup = pd.DataFrame({
        'Year': [1910, 1911, 1912, 1913, 1914, 1915, 1916, 1917, 1918, 1919, 1920],
        'Total Deaths': [0, 0, 2, 3, 2, 3, 4, 5, 6, 7, 8],
        'Total Affected': [0, 1, 0, 2, 3, 6, 9, 8, 12, 13, 15]
    })
    # Add 3-year rolling average
    dfGroup['rolling_3years'] = dfGroup['Total Deaths'].rolling(3).mean().shift(0)
    dfGroup = dfGroup.fillna(0)
    
    # Make a smooth line from the 3-year rolling average
    from scipy.interpolate import make_interp_spline
    
    X_Y_Spline = make_interp_spline(dfGroup['Year'], dfGroup['rolling_3years'])
    
    # Returns evenly spaced numbers over a specified interval.
    X_ = np.linspace(dfGroup['Year'].min(), dfGroup['Year'].max(), 500)
    Y_ = X_Y_Spline(X_)
    
    # Plot the data
    a4_dims = (15, 10)
    fig, ax1 = plt.subplots(figsize=a4_dims)
    sns.barplot(x="Year", y="Total Deaths",
                data=dfGroup, color='#42b7bd', label='Barplot label', ax=ax1)
    x_plot = np.linspace(0, len(dfGroup) - 1, len(X_))
    sns.lineplot(x=x_plot, y=Y_, marker='o', label='LinePlot label', ax=ax1)
    ax1.set_ylim(ymin=0) # let bars touch the bottom of the plot
    ax1.margins(x=0.02) # less margins left and right
    # ax1.legend(title='legend title') # optionally change the legend
    plt.show()
    

    sns.barplot with sns.lineplot and legend

    PS: If an ax is already created, it should be given as a parameter to seaborn's axes-level functions (so, sns.barplot(..., ax=ax1) instead of ax1 = sns.barplot(...).