Search code examples
pythonchartsplotlyfillplotly-python

Plotly: How to color the fill between two lines based on a condition?


I want to add a fill colour between the black and blue line on my Plotly chart. I am aware this can be accomplished already with Plotly but I am not sure how to fill the chart with two colours based on conditions.This is my Plotly chartThis is what I want to achieve

The chart with the blue background is my Plotly chart. I want to make it look like the chart with the white background. (Ignore the red and green bars on the white chart)

The conditions I want it to pass is:

Fill the area between the two lines GREEN, if the black line is above the blue line.

Fill the area between the two lines RED, if the black line is below the blue line.

How can this be done with Plotly? If this is not possible with Plotly can it be accomplished with other graphing tools that work with Python.


Solution

  • For a number of reasons (that I'm willing to explain further if you're interested) the best approach seems to be to add two traces to a go.Figure() object for each time your averages cross eachother, and then define the fill using fill='tonexty' for the second trace using:

    for df in dfs:
        fig.add_traces(go.Scatter(x=df.index, y = df.ma1,
                                  line = dict(color='rgba(0,0,0,0)')))
        
        fig.add_traces(go.Scatter(x=df.index, y = df.ma2,
                                  line = dict(color='rgba(0,0,0,0)'),
                                  fill='tonexty', 
                                  fillcolor = fillcol(df['label'].iloc[0])))
    

    fillcol is a simple custom function described in the full snippet below. And I've used the approach described in How to split a dataframe each time a string value changes in a column? to produce the necessary splits in the dataframe each time your averages cross eachother.

    Plot

    enter image description here

    Complete code:

    import plotly.graph_objects as go
    import numpy as np
    
    import pandas as pd
    from datetime import datetime
    pd.options.plotting.backend = "plotly"
    
    # sample data
    df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/finance-charts-apple.csv')
    df.index = df.Date
    df = df[['AAPL.Close', 'mavg']]
    df['mavg2'] = df['AAPL.Close'].rolling(window=50).mean()
    df.columns = ['y', 'ma1', 'ma2']
    df=df.tail(250).dropna()
    df1 = df.copy()
    
    # split data into chunks where averages cross each other
    df['label'] = np.where(df['ma1']>df['ma2'], 1, 0)
    df['group'] = df['label'].ne(df['label'].shift()).cumsum()
    df = df.groupby('group')
    dfs = []
    for name, data in df:
        dfs.append(data)
    
    # custom function to set fill color
    def fillcol(label):
        if label >= 1:
            return 'rgba(0,250,0,0.4)'
        else:
            return 'rgba(250,0,0,0.4)'
    
    fig = go.Figure()
    
    for df in dfs:
        fig.add_traces(go.Scatter(x=df.index, y = df.ma1,
                                  line = dict(color='rgba(0,0,0,0)')))
        
        fig.add_traces(go.Scatter(x=df.index, y = df.ma2,
                                  line = dict(color='rgba(0,0,0,0)'),
                                  fill='tonexty', 
                                  fillcolor = fillcol(df['label'].iloc[0])))
    
    # include averages
    fig.add_traces(go.Scatter(x=df1.index, y = df1.ma1,
                              line = dict(color = 'blue', width=1)))
    
    fig.add_traces(go.Scatter(x=df1.index, y = df1.ma2,
                              line = dict(color = 'red', width=1)))
    
    # include main time-series
    fig.add_traces(go.Scatter(x=df1.index, y = df1.y,
                              line = dict(color = 'black', width=2)))
    
    fig.update_layout(showlegend=False)
    fig.show()