39

I happened to see a beautiful graph on this page which is shown below:

enter image description here

Is it possible to get such color gradients in matplotlib?

Tom Kurushingal
  • 6,086
  • 20
  • 54
  • 86

3 Answers3

64

There have been a handful of previous answers to similar questions (e.g. https://stackoverflow.com/a/22081678/325565), but they recommend a sub-optimal approach.

Most of the previous answers recommend plotting a white polygon over a pcolormesh fill. This is less than ideal for two reasons:

  1. The background of the axes can't be transparent, as there's a filled polygon overlying it
  2. pcolormesh is fairly slow to draw and isn't smoothly interpolated.

It's a touch more work, but there's a method that draws much faster and gives a better visual result: Set the clip path of an image plotted with imshow.

As an example:

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
from matplotlib.patches import Polygon
np.random.seed(1977)

def main():
    for _ in range(5):
        gradient_fill(*generate_data(100))
    plt.show()

def generate_data(num):
    x = np.linspace(0, 100, num)
    y = np.random.normal(0, 1, num).cumsum()
    return x, y

def gradient_fill(x, y, fill_color=None, ax=None, **kwargs):
    """
    Plot a line with a linear alpha gradient filled beneath it.

    Parameters
    ----------
    x, y : array-like
        The data values of the line.
    fill_color : a matplotlib color specifier (string, tuple) or None
        The color for the fill. If None, the color of the line will be used.
    ax : a matplotlib Axes instance
        The axes to plot on. If None, the current pyplot axes will be used.
    Additional arguments are passed on to matplotlib's ``plot`` function.

    Returns
    -------
    line : a Line2D instance
        The line plotted.
    im : an AxesImage instance
        The transparent gradient clipped to just the area beneath the curve.
    """
    if ax is None:
        ax = plt.gca()

    line, = ax.plot(x, y, **kwargs)
    if fill_color is None:
        fill_color = line.get_color()

    zorder = line.get_zorder()
    alpha = line.get_alpha()
    alpha = 1.0 if alpha is None else alpha

    z = np.empty((100, 1, 4), dtype=float)
    rgb = mcolors.colorConverter.to_rgb(fill_color)
    z[:,:,:3] = rgb
    z[:,:,-1] = np.linspace(0, alpha, 100)[:,None]

    xmin, xmax, ymin, ymax = x.min(), x.max(), y.min(), y.max()
    im = ax.imshow(z, aspect='auto', extent=[xmin, xmax, ymin, ymax],
                   origin='lower', zorder=zorder)

    xy = np.column_stack([x, y])
    xy = np.vstack([[xmin, ymin], xy, [xmax, ymin], [xmin, ymin]])
    clip_path = Polygon(xy, facecolor='none', edgecolor='none', closed=True)
    ax.add_patch(clip_path)
    im.set_clip_path(clip_path)

    ax.autoscale(True)
    return line, im

main()

enter image description here

Community
  • 1
  • 1
Joe Kington
  • 275,208
  • 71
  • 604
  • 463
  • 5
    This is really fantastic! Do you see a way to also make the gradient follow the curve? i.e. instead of `z`'s alpha value stretching evenly from 0 to 1 (in axes coordinates), have `z` stretch from 0 to `y` (in data coordinates)? – unutbu Mar 29 '15 at 16:57
  • 1
    When saving to a vector format, one would need to set `plt.rcParams["image.composite_image"] = False`, else the clipping doesn't work correctly. – ImportanceOfBeingErnest May 31 '19 at 23:41
  • That's really great indeed. – chupa_kabra Jun 24 '19 at 11:51
  • Is the piece of code above supposed to run stand alone? I am not able to execute it and get a whole bunch of errors when I paste it in my Python console. The first error I get is on line 41, not too sure why: ```line, = ax.plot(x, y, **kwargs)``` here is the error ```File "", line 1 line, = ax.plot(x, y, **kwargs) IndentationError: unexpected indent``` – Alexis.Rolland Jan 25 '20 at 04:16
  • It works if I save the file and execute it, I am curious to understand why it does not when I copy paste it in the python console – Alexis.Rolland Jan 25 '20 at 04:19
  • I tried this, but it seems to override my line color all the time. Am I missing something? – n00b.exe Mar 14 '20 at 17:22
20

Please note Joe Kington deserves the lion's share of the credit here; my sole contribution is zfunc. His method opens to door to many gradient/blur/drop-shadow effects. For example, to make the lines have an evenly blurred underside, you could use PIL to build an alpha layer which is 1 near the line and 0 near the bottom edge.

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import matplotlib.patches as patches
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFilter

np.random.seed(1977)
def demo_blur_underside():
    for _ in range(5):
        # gradient_fill(*generate_data(100), zfunc=None) # original
        gradient_fill(*generate_data(100), zfunc=zfunc)
    plt.show()

def generate_data(num):
    x = np.linspace(0, 100, num)
    y = np.random.normal(0, 1, num).cumsum()
    return x, y

def zfunc(x, y, fill_color='k', alpha=1.0):
    scale = 10
    x = (x*scale).astype(int)
    y = (y*scale).astype(int)
    xmin, xmax, ymin, ymax = x.min(), x.max(), y.min(), y.max()

    w, h = xmax-xmin, ymax-ymin
    z = np.empty((h, w, 4), dtype=float)
    rgb = mcolors.colorConverter.to_rgb(fill_color)
    z[:,:,:3] = rgb

    # Build a z-alpha array which is 1 near the line and 0 at the bottom.
    img = Image.new('L', (w, h), 0)  
    draw = ImageDraw.Draw(img)
    xy = np.column_stack([x, y])
    xy -= xmin, ymin
    # Draw a blurred line using PIL
    draw.line(list(map(tuple, xy)), fill=255, width=15)
    img = img.filter(ImageFilter.GaussianBlur(radius=100))
    # Convert the PIL image to an array
    zalpha = np.asarray(img).astype(float) 
    zalpha *= alpha/zalpha.max()
    # make the alphas melt to zero at the bottom
    n = zalpha.shape[0] // 4
    zalpha[:n] *= np.linspace(0, 1, n)[:, None]
    z[:,:,-1] = zalpha
    return z

def gradient_fill(x, y, fill_color=None, ax=None, zfunc=None, **kwargs):
    if ax is None:
        ax = plt.gca()

    line, = ax.plot(x, y, **kwargs)
    if fill_color is None:
        fill_color = line.get_color()

    zorder = line.get_zorder()
    alpha = line.get_alpha()
    alpha = 1.0 if alpha is None else alpha

    if zfunc is None:
        h, w = 100, 1
        z = np.empty((h, w, 4), dtype=float)
        rgb = mcolors.colorConverter.to_rgb(fill_color)
        z[:,:,:3] = rgb
        z[:,:,-1] = np.linspace(0, alpha, h)[:,None]
    else:
        z = zfunc(x, y, fill_color=fill_color, alpha=alpha)
    xmin, xmax, ymin, ymax = x.min(), x.max(), y.min(), y.max()
    im = ax.imshow(z, aspect='auto', extent=[xmin, xmax, ymin, ymax],
                   origin='lower', zorder=zorder)

    xy = np.column_stack([x, y])
    xy = np.vstack([[xmin, ymin], xy, [xmax, ymin], [xmin, ymin]])
    clip_path = patches.Polygon(xy, facecolor='none', edgecolor='none', closed=True)
    ax.add_patch(clip_path)
    im.set_clip_path(clip_path)
    ax.autoscale(True)
    return line, im

demo_blur_underside()

yields

enter image description here

unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
  • Nice! I was going to add a purely vertical shift, but I think I like your gaussian blur of it a lot more. – Joe Kington Apr 01 '15 at 13:21
  • What significance is `scale` in the`zfunc` method? – Jared Nov 10 '15 at 16:21
  • @Jared: The `zfunc` creates a small (blurred) PIL image. The size of the PIL image, `(w, h)`, depends on the differences `xmax-xmin` and `ymax-ymin`. If these differences are too small, then the PIL image will have low resolution. If the resolution is too low, the blur won't look very smooth. So I multiplied the `x` and `y` values by `scale` so that the PIL image size will be bigger. – unutbu Nov 10 '15 at 18:01
  • @unutbu I've tried to replicate your solution here, which looks great, but without much success. I've included a complete, replicable example (but admittedly not all that succinct... I'm working to cut it down but not much can be removed) [here](http://stackoverflow.com/questions/33635688/gradient-fill-under-matplotlib-graphs). Do you know where this glitch comes from? – Jared Dec 01 '15 at 13:53
  • 1
    I would love an explanation of what each piece of this code is doing. I get the general idea, but some of the details aren't clear. – bicarlsen Jun 28 '21 at 11:02
  • 2
    @unutbu With PIL 8.4.0, I get this error: File "./gradient_fill_test.py", line 40, in zfunc draw.line(map(tuple, xy.tolist()), fill=255, width=15) File "./lib/python3.9/site-packages/PIL/ImageDraw.py", line 157, in line self.draw.draw_lines(xy, ink, width) TypeError: argument must be sequence – Tom Kurushingal Nov 13 '21 at 04:22
  • I have the same issue as @TomKurushingal – Freude Jul 16 '22 at 23:05
7

I've tried something :

import matplotlib.pyplot as plt
import numpy as np

fig = plt.figure()

xData = range(100)
yData = range(100)
plt.plot(xData, yData)

NbData = len(xData)
MaxBL = [[MaxBL] * NbData for MaxBL in range(100)]
Max = [np.asarray(MaxBL[x]) for x in range(100)]

for x in range (50, 100):
  plt.fill_between(xData, Max[x], yData, where=yData >Max[x], facecolor='red', alpha=0.02)

for x in range (0, 50):
  plt.fill_between(xData, yData, Max[x], where=yData <Max[x], facecolor='green', alpha=0.02)

plt.fill_between([], [], [], facecolor='red', label="x > 50")
plt.fill_between([], [], [], facecolor='green', label="x < 50")

plt.legend(loc=4, fontsize=12)
plt.show()
fig.savefig('graph.png')

.. and the result:

result

Of course the gradient could go down to 0 by changing the range of feel_between function.

Donyk
  • 79
  • 1
  • 1
  • this working great for me can you provide any fix i am getting some blank when moving to next point in whole area it's not showing https://stackoverflow.com/questions/63061714/can-we-put-this-shade-in-area-chart-seaborn-or-matplotlib-python?noredirect=1#comment111516161_63061714 – conol Jul 23 '20 at 20:00