1

I've gotten a lot of information from these two posts on SO about putting a gradient fill below a curve in matplotlib. I tried the same thing plotting multiple plots on one axis and working the order of them and their alpha to ensure they're visible. I'm getting errors with PIL with this code that outputs this graph: enter image description here

Is it possible to have the 'fill' below the plot come down further, and fix the error in the bottom right corner? I've included the data that I used in this example by putting raw data on bpaste, so even though it's long the example is completely self-contained.

Could it have something to do with the backend used?

Thanks, Jared

import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
from matplotlib.patches import Polygon
from matplotlib.ticker import Formatter, FuncFormatter
import matplotlib
import numpy as np
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFilter


df = pd.read_csv('https://bpaste.net/raw/87cbf69259ae')
df = df.set_index('Date', drop=True)
df.index = pd.to_datetime(df.index)


df1 = pd.read_csv('https://bpaste.net/raw/bc06b26b0b8b')
df1 = df1.set_index('Date', drop=True)
df1.index = pd.to_datetime(df1.index)

def zfunc(x, y, fill_color='k', alpha=1.0, xmin=None, xmax=None, ymin=None, ymax=None):

    if xmax is not None:
        xmax = int(xmax)

    if xmin is not None:
        xmin = int(xmin)

    if ymax is not None:
        ymax = int(ymax)

    if ymin is not None:
        ymin = int(ymin)

    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(map(tuple, xy.tolist()), fill=255, width=15)
    img = img.filter(ImageFilter.GaussianBlur(radius=25))

    # 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 = int(zalpha.shape[0] / 4)

    zalpha[:n] *= np.linspace(0, 10, n)[:, None]
    z[:,:,-1] = zalpha
    return z


def gradient_fill(x, y, fill_color=None, ax=None, ylabel=None, zfunc=None, **kwargs):

    if ax is None:
        ax = plt.gca()

    if ylabel is not None:
        ax.set_ylabel(ylabel, weight='bold', color='white')

    class DateFormatter(Formatter):
        def __init__(self, dates, fmt='%b \'%y'):
            self.dates = dates
            self.fmt = fmt

        def __call__(self, x, pos=0):
            'Return the label for time x at position pos'
            ind = int(round(x))
            if ind>=len(self.dates) or ind<0: return ''

            return self.dates[ind].strftime(self.fmt)

    def millions(x, pos):
        return '$%d' % x

    dollar_formatter = FuncFormatter(millions)     
    formatter = DateFormatter(df.index)
    ax.yaxis.grid(linestyle='-', alpha=0.5, color='white', zorder=-1) 

    line, = ax.plot(x, y, linewidth=2.0, c=fill_color, **kwargs)

    if fill_color is None:
        fill_color = line.get_color()

    zorder = line.get_zorder()
    if 'alpha' in kwargs:
        alpha = kwargs['alpha']
    else:
        alpha = line.get_alpha()
        alpha = 1.0 if alpha is None else alpha

    xmin, xmax, ymin, ymax = x.min(), x.max(), y.min(), y.max()
    diff = ymax - ymin
    ymin = ymin - diff*0.15
    ymax = diff*0.05 + ymax

    if zfunc is None:
        ## Grab an array of length (cols,rows,spacing) but don't initialize values
        z = np.empty((110, 1, 4), dtype=float)
        ## get color to fill for current axix line
        rgb = mcolors.colorConverter.to_rgb(fill_color)
        z[:,:,:3] = rgb
        z[:,:,-1] = np.linspace(0, alpha, 110)[:,None]
    else:
        z = zfunc(x, y, fill_color=fill_color, alpha=alpha, xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax)

    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)
    ax.patch.set_facecolor('black')
    im.set_clip_path(clip_path)


    ax.xaxis.set_major_formatter(formatter)
    ax.yaxis.set_major_formatter(dollar_formatter)

    for tick in ax.get_yticklabels():
        tick.set_color('white')

    for tick in ax.get_xticklabels():
        tick.set_color('white')

    w = 17.5 * 1.5  # approximate size in inches of 1280
    h = 7.5 * 1.5  # approximate size in inches of 720
    fig =  plt.gcf()
    fig.set_size_inches(w, h)
#     fig.autofmt_xdate()
    plt.rcParams['xtick.major.pad']='20'
    matplotlib.rcParams['ytick.major.pad']='20'
    matplotlib.rcParams.update({'font.size': 22})

    ax.set_ylim((ymin, ymax))
    #ax.autoscale(True)
    return line, im, ax

line, im, ax = gradient_fill(np.arange(len(df1.index)), df1['/CL_Close'], fill_color='#fdbf6f', ylabel='Crude Oil', alpha=1.0, zfunc=zfunc)
ax2 = ax.twinx()
gradient_fill(np.arange(len(df.index)), df['/ES_Close'], ax=ax2, fill_color='#cab2d6', ylabel='S&P', alpha=0.75, zfunc=zfunc)
ax2.yaxis.grid(False)
Community
  • 1
  • 1
Jared
  • 3,651
  • 11
  • 39
  • 64

2 Answers2

4

The problem is in your zfunc. You say you want to fade your alphas to zero by multiplying them with np.linspace(0,10,n).

try:

zalpha[:n] *= np.linspace(0, 1, n)[:, None]

then it works for me...

thomas
  • 1,773
  • 10
  • 14
  • Yea, I think that does it. If I want to now extend the fade (but still keep it going to zero), I'm not seeing consistent levels on both curves. [Here](http://i.imgur.com/bP86ZDj.png) is my image outputted -- the orange looks great, the purple is not as consistent. – Jared Dec 01 '15 at 14:24
  • 1
    That's probably because you have `alpha = 0.75` for the second line. So it will fade more quickly. You could gain more control if you add another parameter `alpha_min` and use that as the first argument to `linspace` above. – thomas Dec 01 '15 at 14:39
1

It is a different approach than what you have taken, but perhaps you can use an image with varying intensity and a colormap using an alpha value like this:

import numpy as np
import scipy as sc

import matplotlib.pyplot as plt

x = np.linspace (0, 10, 100)
y = .5 * x + 4

plt.figure ()


yres = 100
ymax = np.max (y)
ymin = 0 
yy = np.linspace (ymin, ymax, yres)

fill_n = 10

xres = len(x)

# gradient image
gI = np.zeros ((yres, xres))
for xi,xx in enumerate(x):
  ym = y[xi]

  # find elment closest to curve
  ya = np.argmin (np.abs(yy - ym))

  gI[ya-fill_n:ya, xi] = np.linspace (0, 1, fill_n)

# make alpha cmap out of gray map
bb = np.linspace (0, 1, fill_n)
kk = []
for b in bb:
  kk.append ((b, b, b))

bb = tuple (kk) 
gr = { 'blue' : bb,
       'red' : bb,
       'green': bb,
       'alpha': bb }

plt.register_cmap (name = 'GrayAlpha', data = gr)

gI = np.flipud (gI)
plt.imshow (gI, vmin = 0, vmax = 1, cmap = 'GrayAlpha', interpolation = 'bicubic')
plt.show ()

enter image description here

gauteh
  • 16,435
  • 4
  • 30
  • 34