Search code examples
pythonpandasplotlyplotly-python

Python Plotly fill between two specific lines


I've seen this question asked before in some form or the other, but never answered entirely, so i'll try my luck.

In a plot with several traces, i’m trying to fill the areas between two specific lines that i choose. The desires effect, achieved with matplotlib.pyplot looks like this:

desired plot

where the cyan and orange areas are filled between their respective trends, and up/down to the red line. The black line is unrelated to those filled areas, but should be plotted along with the red line regardless.

I tried achieving this with Plotly’s fill: 'tonexty', but this doesn’t work out well because due to the randomness of the data, the nearest Y is sometimes the black line and not the red.

I can achieve this with pyplot's fill_between function: plt.fill_between(x, y1, y2)

Where it can be used as:

from matplotlib import pyplot as plt
import numpy as np
import pandas as pd

series1 = pd.Series(np.random.randn(100), index=pd.date_range('1/1/2000', periods=100))

series2 = series1 + np.random.randn(len(series1))

positive_contribution = series2 + 0.5
negative_contribution = series2 - 0.5

plt.plot(series1, color='black')
plt.plot(series2, color='red')
plt.fill_between(series2.index, series2, negative_contribution, color='cyan', alpha=0.2)
plt.fill_between(series2.index, series2, positive_contribution, color='orange', alpha=0.2)

I’d love to hear of a solution / workaround for this.


Solution

  • A way to achieve this is to add traces in the right order. An important thing I noticed is that "tonexty" actually fill the space between the current trace and the trace that was previously added in the 'trace storage'.

    The first option is thus to add your traces from top to bottom or bottom to top (I chose bottom to top, this is partially extracted from the plotly documentation):

    
    # the cyan trace, without color filling
    fig.add_trace(
        go.Scatter(
            x=series2.index,
            y=negative_contribution,
            fill=None,
            mode="lines",
            line_color="cyan",
        )
    )
    
    # the red trace, with cyan color filling up to the previously added trace
    fig.add_trace(
        go.Scatter(
            x=series2.index,
            y=series2,
            fill="tonexty",
            fillcolor="cyan",
            mode="lines",
            line_color="red",
        )
    )
    
    # the orange trace, with orange color filling to the previously added trace (the red one).
    fig.add_trace(
        go.Scatter(
            x=series2.index,
            y=positive_contribution,
            fill="tonexty",  # fill area between trace0 and trace1
            mode="lines",
            line_color="orange",
        )
    )
    
    # add the black trace.
    fig.add_trace(
        go.Scatter(
            x=series1.index,
            y=series1,
            fill=None,
            mode="lines",
            line_color="black",
        )
    )
    

    A problem with this solution is that the cyan color filling legend will be merged with the red trace, which can be confusing.

    Another solution is to always add the trace you want to fill between just before the tonexty trace. You should use a transparent color, use showlegend=False for any trace but the final trace added (if a trace need to be repeteadly added):

    # add a transparent red trace not visible on legend.
    fig.add_trace(
        go.Scatter(
            x=series2.index,
            y=series2,
            fill=None,
            mode="lines",
            line_color="rgba(0,0,0,0)",
            showlegend=False,
        )
    )
    # add orange trace
    fig.add_trace(
        go.Scatter(
            x=series2.index,
            y=positive_contribution,
            fill="tonexty",
            mode="lines",
            line_color="orange",
        )
    )
    # re add the red trace, but visible this time, as this is the last one
    fig.add_trace(
        go.Scatter(
            x=series2.index,
            y=series2,
            fill=None,
            mode="lines",
            line_color="red",
        )
    )
    # add the cyan trace
    fig.add_trace(
        go.Scatter(
            x=series2.index,
            y=negative_contribution,
            fill="tonexty",
            mode="lines",
            line_color="cyan",
        )
    )
    # finally add the black trace
    fig.add_trace(
        go.Scatter(
            x=series1.index,
            y=series1,
            fill=None,
            mode="lines",
            line_color="black",
        )
    )
    
    

    It is more controllable, however, adding multiple time the same trace could add spurious interactive effect.