Search code examples
matplotliblegendalphafillbetween

Correct legend color for intersecting transparent layers in Matplotlib


I often need to indicate the distribution of some data in a concise plot, as in the below figure. I do this by plotting several fill_between areas, limited by the quantiles of the distribution.

ax.fill_between(x, quantile1, quantile2, alpha=0.2)

In a for loop, I make plots like this by calculating quantiles 1 and 2 (as indicated by the legend) as the 0% to 100% quantiles, then 10% to 90% and so on, each fill_between plotting on top of the previous "layer".

Here is the output with three layers of transparent colors along with the median line (0.5):
Three layers of transparent colors along with the median line (0.5)

However, the legend colors are not what I would like them to be, since they (naturally) use the color of each individual layer, not taking into account the combined effect of several layers.

ax.legend([0.5]+[['0.0%', '100.0%'], ['10.0%', '90.0%'], ['30.0%', '70.0%']])

What is the best way to overwrite the face color value within the legend command?

I would like to avoid doing this by first plotting 0% to 10% with transparency "0.2", then 10% to 30% with transparency "0.4" and so on, as this will take twice the amount of time to compute and will make the code more complicated.


Solution

  • You can use proxy artists to place in the legend which have the exact same transparency as the resulting overlay from the plot.
    As a proxy artist you can use a simple rectangle. The transparency however needs to be calculated as two objects with transparency 0.2 together will appear as a single object with transparency 0.36 (and not 0.4!).

    import matplotlib.pyplot as plt
    import numpy as np
    import matplotlib.patches
    
    a = np.sort(np.random.rand(6,18), axis=0)
    x = np.arange(len(a[0]))
    
    def alpha(i, base=0.2):
        l = lambda x: x+base-x*base
        ar = [l(0)]
        for j in range(i):
            ar.append(l(ar[-1]))
        return ar[-1]
    
    fig, ax = plt.subplots(figsize=(4,2))
    
    handles = []
    labels=[]
    for i in range(len(a)/2):
        ax.fill_between(x, a[i, :], a[len(a)-1-i, :], color="blue", alpha=0.2)
        handle = matplotlib.patches.Rectangle((0,0),1,1,color="blue", alpha=alpha(i, base=0.2))
        handles.append(handle)
        label = "quant {:.1f} to {:.1f}".format(float(i)/len(a)*100, 100-float(i)/len(a)*100)
        labels.append(label)
    
    plt.legend(handles=handles, labels=labels, framealpha=1)
    plt.show()
    

    enter image description here


    One has to decide if this is really worth the effort. A solution without transparency but with the very same result can be achieved much shorter:

    import matplotlib.pyplot as plt
    import numpy as np
    
    a = np.sort(np.random.rand(6,18), axis=0)
    x = np.arange(len(a[0]))
    
    fig, ax = plt.subplots(figsize=(4,2))
    
    for i in range(len(a)/2):
        label = "quant {:.1f} to {:.1f}".format(float(i)/len(a)*100, 100-float(i)/len(a)*100)
        c = plt.cm.Blues(0.2+.6*(float(i)/len(a)*2)  )
        ax.fill_between(x, a[i, :], a[len(a)-1-i, :], color=c, label=label)
    
    plt.legend( framealpha=1)
    plt.show()
    

    enter image description here