Skip to content

add drawstyle option to fill_between function #643

Closed

Description

@sbarthelemy

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

Activity

  1. astrofrog commented on Jul 8, 2012

    @astrofrog
    Contributor

    +1

  2. pelson commented on Sep 2, 2012

    @pelson
    Member

    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.

  3. ycopin commented on Oct 8, 2012

    @ycopin

    +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.

  4. thriveth commented on Sep 25, 2013

    @thriveth

    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?

  5. WeatherGod commented on Oct 1, 2013

    @WeatherGod
    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.

  6. mdboom commented on Oct 1, 2013

    @mdboom
    Member

    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.

  7. thriveth commented on Oct 1, 2013

    @thriveth

    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.

  8. thriveth commented on Jan 10, 2014

    @thriveth

    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

  9. added this to the milestone on Aug 18, 2014
  10. tacaswell commented on Aug 18, 2014

    @tacaswell
    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)
    
  11. thriveth commented on Aug 25, 2014

    @thriveth

    @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.

  12. thriveth commented on Aug 25, 2014

    @thriveth

    @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.

  13. theodoregoetz commented on Sep 4, 2014

    @theodoregoetz

    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

  14. theodoregoetz commented on Sep 4, 2014

    @theodoregoetz

    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

  15. thriveth commented on Sep 5, 2014

    @thriveth

    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

  16. tacaswell commented on Nov 20, 2014

    @tacaswell
    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

  17. self-assigned this
    on Dec 2, 2014
  18. dguest commented on May 10, 2015

    @dguest

    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.

  19. modified the milestones: , on May 11, 2015
  20. tacaswell commented on May 11, 2015

    @tacaswell
    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.

  21. efiring commented on May 11, 2015

    @efiring
    Member

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

  22. added 2 commits that reference this issue on May 15, 2015
    c16050f
    efced7d
  23. added 2 commits that reference this issue on Jul 16, 2015
    493c4d1
    f8ae4ff
  24. pollackscience commented on Nov 12, 2015

    @pollackscience

    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

Metadata

Metadata

Assignees

Labels

API: consistencyConsistency of the matplotlib API, including naming, behavior, defaults, …New feature

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions