Search code examples
pythonseaborndata-visualizationboxplotcatplot

How to add hatches to boxplots with sns.boxplot or sns.catplot


I need to add hatches to a categorical box plot. What I have is this: enter image description here

What I need is something like this (with the median lines):

enter image description here

And what I have tried is this code:

exercise = sns.load_dataset("exercise")
g = sns.catplot(x="time", y="pulse", hue="kind", data=exercise, kind="box")
bars = g.axes[0][0].patches
hatches=['//','..','xx','//','..','xx','//','..','xx']
for pat,bar in zip(hatches,bars):
    bar.set_hatch(pat)

That only generates the first figure. The idea for lines 3-6 comes from this question. But the idea to get axes[0][0] in line 3 comes from this question.

Because FacetGrids don't have attributes like patches or containers, it makes it harder to adapt the answers about hatches in individual plots to categorical plots, so I couldn't figure it out.

Other reviewed questions that don't work:


Solution

    1. Iterate through each subplot / FacetGrid with for ax in g.axes.flat:.
    2. ax.patches contains matplotlib.patches.Rectangle and matplotlib.patches.PathPatch, so the correct ones must be used.
    • Caveat: all hues must appear for each group in each Facet, otherwise the patches and hatches will not match.
      • In this case, manual or conditional code will probably be required to correctly determine h, so zip(patches, h) works.
    • Tested in python 3.10, pandas 1.4.2, matplotlib 3.5.1, seaborn 0.11.2
    import matplotlib as mpl
    import seaborn as sns
    
    # load test data
    exercise = sns.load_dataset("exercise")
    
    # plot
    g = sns.catplot(x="time", y="pulse", hue="kind", data=exercise, col='diet', kind="box")
    
    # hatches must equal the number of hues (3 in this case)
    hatches = ['//', '..', 'xx']
    
    # iterate through each subplot / Facet
    for ax in g.axes.flat:
    
        # select the correct patches
        patches = [patch for patch in ax.patches if type(patch) == mpl.patches.PathPatch]
        # the number of patches should be evenly divisible by the number of hatches
        h = hatches * (len(patches) // len(hatches))
        # iterate through the patches for each subplot
        for patch, hatch in zip(patches, h):
            patch.set_hatch(hatch)
            fc = patch.get_facecolor()
            patch.set_edgecolor(fc)
            patch.set_facecolor('none')
    

    enter image description here

    • Add the following, to change the legend.
    for lp, hatch in zip(g.legend.get_patches(), hatches):
        lp.set_hatch(hatch)
        fc = lp.get_facecolor()
        lp.set_edgecolor(fc)
        lp.set_facecolor('none')
    

    enter image description here


    • If only using the axes-level sns.boxplot, there's no need to iterate through multiple axes.
    ax = sns.boxplot(x="time", y="pulse", hue="kind", data=exercise)
    
    # select the correct patches
    patches = [patch for patch in ax.patches if type(patch) == mpl.patches.PathPatch]
    # the number of patches should be evenly divisible by the number of hatches
    h = hatches * (len(patches) // len(hatches))
    # iterate through the patches for each subplot
    for patch, hatch in zip(patches, h):
        patch.set_hatch(hatch)
        fc = patch.get_facecolor()
        patch.set_edgecolor(fc)
        patch.set_facecolor('none')
    
    l = ax.legend()
        
    for lp, hatch in zip(l.get_patches(), hatches):
        lp.set_hatch(hatch)
        fc = lp.get_facecolor()
        lp.set_edgecolor(fc)
        lp.set_facecolor('none')
    

    enter image description here


    • To keep the facecolor of the box plots:
      1. Remove patch.set_facecolor('none')
      2. Set the edgecolor as 'k' (black) instead of fc, patch.set_edgecolor('k').
      • Applies to the sns.catplot code too.
    ax = sns.boxplot(x="time", y="pulse", hue="kind", data=exercise)
    
    # select the correct patches
    patches = [patch for patch in ax.patches if type(patch) == mpl.patches.PathPatch]
    # the number of patches should be evenly divisible by the number of hatches
    h = hatches * (len(patches) // len(hatches))
    # iterate through the patches for each subplot
    for patch, hatch in zip(patches, h):
        patch.set_hatch(hatch)
        patch.set_edgecolor('k')
        
    l = ax.legend()
        
    for lp, hatch in zip(l.get_patches(), hatches):
        lp.set_hatch(hatch)
        lp.set_edgecolor('k')
    

    enter image description here