Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add drawstyle option to fill_between function #643

Closed
sbarthelemy opened this issue Dec 28, 2011 · 19 comments
Closed

add drawstyle option to fill_between function #643

sbarthelemy opened this issue Dec 28, 2011 · 19 comments

Comments

@sbarthelemy
Copy link

Hi,

I think the fill_between function should take a drawstyle option. It would be both handy and consistent.

Here is a post from someone else requesting the feature, with a figure illustrating why this is needed:
http://old.nabble.com/fill_between-with-drawstyle-steps--td30172277.html

thank you

@astrofrog
Copy link
Contributor

+1

@pelson
Copy link
Member

pelson commented Sep 2, 2012

I notice lines have a drawstyle, but patches do not. In short, to implement this functionality, I believe one would want to implement drawstyles on patches.

@ycopin
Copy link

ycopin commented Oct 8, 2012

+1

I don't think it's needed to implement drawstyles on patches (this is probably not well defined anyway). One should rather get back the x,y coordinates of the step-lines (i.e [x0,x1,x1,x2,x2,...],[y0,y0,y1,y1,y2,...]) and give it to fill_between.

@thriveth
Copy link

Hello,
I am the one who wrote the request linked in the original post, and I still frequently run into situations where it would be handy to have. I see a few feature requests here that have not been active for half a year or so, how should we interpret this?

@WeatherGod
Copy link
Member

I think what would need to be done is a MEP describing exactly what a drawstyle would mean for a Patch object. I am not familiar enough with what exactly a drawstyle means (as opposed to a linestyle). Should it also be included for Polygons? What about Collections? These things need to be worked out prior to adding the top-level feature to fill_between.

@mdboom
Copy link
Member

mdboom commented Oct 1, 2013

I don't think drawstyles on all patches makes sense (on an ellipse? what would that mean?). However, it may make sense to have a "FilledLine" class that inherits from line, except is filled above or below (maybe left or right as well). This would then get all of the features of Line.

@thriveth
Copy link

thriveth commented Oct 1, 2013

Maybe go a different route and add a drawstyle keyword to the function rather than the object, and return a simple Patch object? The cost would of course be that you couldn't easily change from filled-steps to normal fill-between, but at least it would make it relatively simple to create said filled step plot. At least in my case, having the simple functionality is really all that matters.

@thriveth
Copy link

Any news about this?

I made a little convenience function that works in most cases - it's primitive but gets the work done, and that is what I care about right now. https://gist.github.com/thriveth/8352565
fill_between_steps

@tacaswell tacaswell added this to the v1.5.x milestone Aug 18, 2014
@tacaswell
Copy link
Member

@thriveth What cases does that method not work in?

Copied the code (with minor edit to fix return value) from the linked just below just in case.

def fill_between_steps(x, y1, y2=0, h_align='mid', ax=None, **kwargs):
    ''' Fills a hole in matplotlib: Fill_between for step plots.

    Parameters :
    ------------

    x : array-like
        Array/vector of index values. These are assumed to be equally-spaced.
        If not, the result will probably look weird...
    y1 : array-like
        Array/vector of values to be filled under.
    y2 : array-Like
        Array/vector or bottom values for filled area. Default is 0.

    **kwargs will be passed to the matplotlib fill_between() function.

    '''
    # If no Axes opject given, grab the current one:
    if ax is None:
        ax = plt.gca()
    # First, duplicate the x values
    xx = x.repeat(2)[1:]
    # Now: the average x binwidth
    xstep = (x[1:] - x[:-1]).mean()
    # Now: add one step at end of row.
    xx = sp.append(xx, xx.max() + xstep)

    # Make it possible to change step alignment.
    if h_align == 'mid':
        xx -= xstep / 2.
    elif h_align == 'right':
        xx -= xstep

    # Also, duplicate each y coordinate in both arrays
    y1 = y1.repeat(2)
    if type(y2) == sp.ndarray:
        y2 = y2.repeat(2)

    # now to the plotting part:
    return ax.fill_between(xx, y1, y2=y2, **kwargs)

@thriveth
Copy link

@tacaswell I don't know if there are any, I just haven't tested it thoroughly. But I am using that method extensively in my current work.

@thriveth
Copy link

@tacaswell For one thing, it only works for evenly spaced data points. I suspect it should be possible to extend so it would work for unevenly spaced data too but I haven\t had any need for it so far so I have not looked into it.

@theodoregoetz
Copy link

My workaround for this uses PathPatch which has the ability to handle nonuniform steps - also the potential to create a collection which allows the blending of alphas (needed when rasterizing for use in eps figures). Here is a quick and dirty example. One could change y0 to an array and have the points iterate backwards to complete the path if you really wanted fill between two "curves" and not just fill between the data and a constant value.

When creating these plots, I typically use the keyword "polygon" for drawstyle. This might be the word you are looking for here, or it might be too general since all the angles are 90 degrees.

import numpy as np
from numpy.random import rand
import itertools as it
from scipy import stats
import matplotlib.pyplot as plt

from matplotlib.path import Path
from matplotlib.patches import PathPatch

np.random.seed(1)
data = stats.gamma(3).rvs(5000)

h,e = np.histogram(data,70)

x = e
y0 = 0
y1 = h

xx = it.chain.from_iterable(it.izip(*it.tee(x)))
yy = it.chain.from_iterable(it.izip(*it.tee(y1)))

next(xx,None)

points = list(it.chain(
    [(x[0],y0)],
    it.izip(xx,yy),
    [(x[-1],y0),
     (x[0],y0),
     (x[0],y0)]))

extent = (min(x),max(x),min([y0,min(y1)]),max([y0,max(y1)]))

codes = [Path.MOVETO] \
      + [Path.LINETO]*(len(points) - 3) \
      + [Path.MOVETO,
         Path.CLOSEPOLY]

fig = plt.figure()
ax = fig.add_subplot(1,1,1)
data_patch = PathPatch(Path(points, codes), color='steelblue', alpha=0.6, linewidth=0)
_ = ax.add_patch(data_patch)
_ = ax.set_xlim(extent[0],extent[1])
_ = ax.set_ylim(extent[2],1.05*extent[3])

### compare to this:
#_ = ax.hist(data, 70, histtype="stepfilled", edgecolor='none',alpha=.7)

### also to this:
#_ = ax.plot(e,list(h)+[h[-1]],linestyle='steps-post',color='red',alpha=0.9)

hist

@theodoregoetz
Copy link

For completeness, here is an example for the fill_between two curves case:

import numpy as np
from numpy.random import rand
import itertools as it
from scipy import stats
import matplotlib.pyplot as plt

from matplotlib.path import Path
from matplotlib.patches import PathPatch

np.random.seed(1)
data0 = stats.gamma(3).rvs(5000)
data1 = stats.gamma(3).rvs(10000)

h0,e = np.histogram(data0,70)
h1,_ = np.histogram(data1,e)

x  = e
y0 = h0
y1 = h1

xx  = list(it.chain.from_iterable(it.izip(*it.tee(x))))
yy0 = list(it.chain.from_iterable(it.izip(*it.tee(y0))))
yy1 = list(it.chain.from_iterable(it.izip(*it.tee(y1))))

points = list(it.chain(
    [(xx[0],yy0[0])],
    it.izip(xx[1:],yy1),
    it.izip(xx[-2::-1],yy0[::-1]),
    [(xx[0],yy0[0])]))

extent = (min(x),max(x),min([min(y0),min(y1)]),max([max(y0),max(y1)]))

codes = [Path.MOVETO] \
      + [Path.LINETO]*(len(points) - 3) \
      + [Path.MOVETO,
         Path.CLOSEPOLY]

fig = plt.figure()
ax = fig.add_subplot(1,1,1)
data_patch = PathPatch(Path(points, codes), facecolor='steelblue', alpha=0.6, linewidth=0)
_ = ax.add_patch(data_patch)
_ = ax.set_xlim(extent[0],extent[1])
_ = ax.set_ylim(extent[2],1.05*extent[3])

# compare with:
#_ = ax.hist(data0, 70, histtype="stepfilled", edgecolor='none',alpha=.7)
#_ = ax.hist(data1, e, histtype="stepfilled", edgecolor='none',alpha=.7)
#_ = ax.plot(e,list(h0)+[h0[-1]],linestyle='steps-post',color='red',alpha=0.9)
#_ = ax.plot(e,list(h1)+[h1[-1]],linestyle='steps-post',color='red',alpha=0.9)

hist2

@thriveth
Copy link

thriveth commented Sep 5, 2014

Turns out, my function was fairly simple to generalize to unevenly spaced data. I have updated the gist and include the code below.

def fill_between_steps(x, y1, y2=0, h_align='mid', ax=None, **kwargs):
    ''' Fills a hole in matplotlib: fill_between for step plots.

    Parameters :
    ------------

    x : array-like
        Array/vector of index values. These are assumed to be equally-spaced.
        If not, the result will probably look weird...
    y1 : array-like
        Array/vector of values to be filled under.
    y2 : array-Like
        Array/vector or bottom values for filled area. Default is 0.

    **kwargs will be passed to the matplotlib fill_between() function.

    '''
    # If no Axes opject given, grab the current one:
    if ax is None:
        ax = plt.gca()
    # First, duplicate the x values
    xx = x.repeat(2)[1:]
    # Now: the average x binwidth
    xstep = sp.repeat((x[1:] - x[:-1]), 2)
    xstep = sp.concatenate(([xstep[0]], xstep, [xstep[-1]]))
    # Now: add one step at end of row.
    xx = sp.append(xx, xx.max() + xstep[-1])

    # Make it possible to chenge step alignment.
    if h_align == 'mid':
        xx -= xstep / 2.
    elif h_align == 'right':
        xx -= xstep

    # Also, duplicate each y coordinate in both arrays
    y1 = y1.repeat(2)#[:-1]
    if type(y2) == sp.ndarray:
        y2 = y2.repeat(2)#[:-1]

    # now to the plotting part:
    ax.fill_between(xx, y1, y2=y2, **kwargs)

    return ax

This still takes all the samme keywords as fill_between.

Here is an example, with a plot of the lower data in orange with drawstyle='steps-mid' set, they match very nicely.
fill_between_steps_test

@tacaswell
Copy link
Member

Here is a more complete solution which deals with all of the stepping methods of

def fill_between_steps(ax, x, y1, y2=0, step_where='pre', **kwargs):
    ''' fill between a step plot and 

    Parameters
    ----------
    ax : Axes
       The axes to draw to

    x : array-like
        Array/vector of index values.

    y1 : array-like or float
        Array/vector of values to be filled under.
    y2 : array-Like or float, optional
        Array/vector or bottom values for filled area. Default is 0.

    step_where : {'pre', 'post', 'mid'}
        where the step happens, same meanings as for `step`

    **kwargs will be passed to the matplotlib fill_between() function.

    Returns
    -------
    ret : PolyCollection
       The added artist

    '''
    if step_where not in {'pre', 'post', 'mid'}:
        raise ValueError("where must be one of {{'pre', 'post', 'mid'}} "
                         "You passed in {wh}".format(wh=step_where))

    # make sure y values are up-converted to arrays 
    if np.isscalar(y1):
        y1 = np.ones_like(x) * y1

    if np.isscalar(y2):
        y2 = np.ones_like(x) * y2

    # temporary array for up-converting the values to step corners
    # 3 x 2N - 1 array 

    vertices = np.vstack((x, y1, y2))

    # this logic is lifted from lines.py
    # this should probably be centralized someplace
    if step_where == 'pre':
        steps = ma.zeros((3, 2 * len(x) - 1), np.float)
        steps[0, 0::2], steps[0, 1::2] = vertices[0, :], vertices[0, :-1]
        steps[1:, 0::2], steps[1:, 1:-1:2] = vertices[1:, :], vertices[1:, 1:]

    elif step_where == 'post':
        steps = ma.zeros((3, 2 * len(x) - 1), np.float)
        steps[0, ::2], steps[0, 1:-1:2] = vertices[0, :], vertices[0, 1:]
        steps[1:, 0::2], steps[1:, 1::2] = vertices[1:, :], vertices[1:, :-1]

    elif step_where == 'mid':
        steps = ma.zeros((3, 2 * len(x)), np.float)
        steps[0, 1:-1:2] = 0.5 * (vertices[0, :-1] + vertices[0, 1:])
        steps[0, 2::2] = 0.5 * (vertices[0, :-1] + vertices[0, 1:])
        steps[0, 0] = vertices[0, 0]
        steps[0, -1] = vertices[0, -1]
        steps[1:, 0::2], steps[1:, 1::2] = vertices[1:, :], vertices[1:, :]
    else:
        raise RuntimeError("should never hit end of if-elif block for validated input")

    # un-pack
    xx, yy1, yy2 = steps

    # now to the plotting part:
    return ax.fill_between(xx, yy1, y2=yy2, **kwargs)

fig, ax_list = plt. subplots(3, 1)
x = y = np.arange(5)

for ax, where in zip(ax_list, ['pre', 'post', 'mid']):
    ax.step(x, y, where=where, color='r', zorder=5, lw=5)
    fill_between_steps(ax, x, y, 0, step_where=where)

so

I will get around to putting this in a PR eventually

@dguest
Copy link

dguest commented May 10, 2015

This would be a very nice addition, @tacaswell do you think this would be implemented soon? I've written something similar myself (and step style is more or less required in my field, so I'm sure quite a few others have as well), but it would be great to see a standard solution.

@tacaswell tacaswell modified the milestones: next point release, proposed next point release May 11, 2015
@tacaswell
Copy link
Member

I would just use the function above for now. The only hold up is I am not sure if this should be it's own function step_fill_between or as a flag in the current fill_between.

@efiring
Copy link
Member

efiring commented May 11, 2015

I would vote for an additional kwarg in fill_between, and presumably in fill_betweenx as well.

tacaswell added a commit to tacaswell/matplotlib that referenced this issue May 15, 2015
Add ability to fill between 'step' plots.

Closes matplotlib#643 and matplotlib#1709
tacaswell added a commit to tacaswell/matplotlib that referenced this issue May 15, 2015
Add ability to fill between 'step' plots.

Closes matplotlib#643 and matplotlib#1709
tacaswell added a commit to tacaswell/matplotlib that referenced this issue Jul 16, 2015
Add ability to fill between 'step' plots.

Closes matplotlib#643 and matplotlib#1709
@pollackscience
Copy link

Great solution @tacaswell. I would additionally request the ability to specify N+1 step edges for N step values. Currently I am using your solution by plotting the first bin with step_where='post', then the remaining bins with step_where='pre'. Also, the poly collection that is specified by fill_between does not support legend(), which is unfortunate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests