Search code examples
pythonhistogramseaborngradientfacet-grid

Gradient fill underneath each histogram curve - Python


I am trying to incorporate a gradient fill with multiple histograms using seaborn facet grid where the gradient is determined by the spread of values under each curve, not just by a sequence of row or col using hue. There are some links below that partly perform somewhat similar functions in python:

How to fill histogram with gradient color fills a diverging gradient but each histogram is independent of the others so comparison between histograms is somewhat void. Using the figure below each histogram should be relative to the others. Furthermore, it does not use the seaborn facet grid, which is the central question here.

How to generate series of histograms doesn't plot histograms. It just fills the area under a curve.

I've found a few images displaying what I'm hoping to execute but they all seem to be generated in R with nothing in python. My assumption is the functionality doesn't exist as yet using seaborn and I'll have to use R but I think this will be applicable for many users.

enter image description here

Using the code below, we can change adjust the gradient using hue to either row or col but this doesn't consider the area under the curve.

import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np

# Create the data
rs = np.random.RandomState(1979)
x = rs.randn(120)
g = np.tile(list("ABCD"), 30)
h = np.tile(list("XYZ"), 40)

# Generate df
df = pd.DataFrame(dict(x = x, g = g, h = h))

# Initialize the FacetGrid object
pal = sns.cubehelix_palette(4, rot = -0.25, light = 0.7)
g = sns.FacetGrid(df, col = 'h', hue = 'h', row = 'g', aspect = 3, height= 1, palette = pal)

# Draw the densities 
g = g.map(sns.kdeplot, 'x', shade = True, alpha = 0.8, lw = 1, bw = 0.8)
g = g.map(sns.kdeplot, 'x', color= 'w', lw = 1, bw = 0.8)
g = g.map(plt.axhline, y = 0, lw = 1)

# Adjust title and axis labels directly
g.axes[0,0].set_ylabel('L 1')
g.axes[1,0].set_ylabel('L 2')
g.axes[2,0].set_ylabel('L 3')
g.axes[3,0].set_ylabel('L 4')

g.axes[0,0].set_title('Top 1')
g.axes[0,1].set_title('Top 2')
g.axes[0,2].set_title('Top 3')

g.axes[1,0].set_title('')
g.axes[1,1].set_title('')
g.axes[1,2].set_title('')
g.axes[2,0].set_title('')
g.axes[2,1].set_title('')
g.axes[2,2].set_title('')
g.axes[3,0].set_title('')
g.axes[3,1].set_title('')
g.axes[3,2].set_title('')

g.set_axis_labels(x_var = 'Total Amount')
g.set(yticks = [])

Out:

There is a gradient that can be adjusted for row or col but I'm hoping to pass this gradient to the area underneath each histogram curve. Similar to the figure above. So the area underneath each curve would be lighter when lower than zero and darker when higher than zero.

Even adjusting the area under the curve to the median value may suffice.

enter image description here


Solution

  • You can create an image gradient, and use the histogram itself as a clipping path for the image, so that the only visible part is the part under the curve.

    As such, you can play around with any cmaps and normalization that are available when creating images.

    Here is a quick example:

    import pandas as pd
    import seaborn as sns
    import matplotlib.pyplot as plt
    import numpy as np
    
    # Create the data
    rs = np.random.RandomState(1979)
    x = rs.randn(120)
    g = np.tile(list("ABCD"), 30)
    h = np.tile(list("XYZ"), 40)
    
    # Generate df
    df = pd.DataFrame(dict(x = x, g = g, h = h))
    
    # Initialize the FacetGrid object
    pal = sns.cubehelix_palette(4, rot = -0.25, light = 0.7)
    g = sns.FacetGrid(df, col = 'h', hue = 'h', row = 'g', aspect = 3, height= 1, palette = pal)
    
    # Draw the densities 
    g = g.map(sns.kdeplot, 'x', shade = True, alpha = 0.8, lw = 1, bw = 0.8)
    g = g.map(sns.kdeplot, 'x', color= 'w', lw = 1, bw = 0.8)
    g = g.map(plt.axhline, y = 0, lw = 1)
    
    for ax in g.axes.flat:
        ax.set_title("")
    
    # Adjust title and axis labels directly
    for i in range(4):
        g.axes[i,0].set_ylabel('L {:d}'.format(i))
    for i in range(3):
        g.axes[0,i].set_title('Top {:d}'.format(i))
    
    
    
    # generate a gradient
    cmap = 'coolwarm'
    x = np.linspace(0,1,100)
    for ax in g.axes.flat:
        im = ax.imshow(np.vstack([x,x]), aspect='auto', extent=[*ax.get_xlim(), *ax.get_ylim()], cmap=cmap, zorder=10)
        path = ax.collections[0].get_paths()[0]
        patch = matplotlib.patches.PathPatch(path, transform=ax.transData)
        im.set_clip_path(patch)
    
    g.set_axis_labels(x_var = 'Total Amount')
    g.set(yticks = [])
    

    enter image description here