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

Question:

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.

Asked By: Patrick Miller

||

Answers:

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()
Answered By: vestland

[This is a javascript solution of the problem]

I went for a completely different approach to create climate charts.

I have used a set of functions that check if two traces intersect each other. For each intersection, the function will take all points of the traces until the intersection point to create seperate polygons and colour them in.
This function is recursively. If there is no intersection, there will only be one polygon, if there is one intersection, there are two polygons, if there are two intersections, there are three polygons, etc. These polygons are then added to the chart.

An introduction to shapes in plotly is given here:
https://plotly.com/javascript/shapes/

I used an existing function for finding intersection points from here: https://stackoverflow.com/a/38977789/3832675

I wrote my own function for creating the polygon strings that create the path strings necessary to the polygons. Depending on which line is above the other (which can be realised using a simple comparison), the variable "colour" is either green or red.

function draw_and_colour_in_polygon(temperature_values, precipitation_values, x_values) {

        var path_string = '';
        for (var i = 0; i < x_values.length; i++) {
            path_string += ' L ' + x_values[i] + ', '+ temperature_values[i];
        }
        for (var i = precipitation_values.length-1; i >= 0; i--) {
            path_string += ' L ' + x_values[i] + ', ' + precipitation_values[i];
        }
        
        path_string += ' Z';
        path_string = path_string.replace(/^.{2}/g, 'M ');
        
        if (temperature_values[0] >= precipitation_values[0] && temperature_values[1] >= precipitation_values[1]) {
            var colour = 'rgba(255, 255, 0, 0.5)';
        }
        else {
            var colour = 'rgba(65, 105, 225, 0.5)';
        }
        
        return {
            path: path_string,
            polygon_colour: colour,
        }; 
};

All of this put together looks like this:
Plotly - Climate chart

In this case, we have three seperate polygons added to the chart. They are either blue or yellow depending on whether temperature value is higher than the precipiation value or vice versa. Please bear in mind that the polygons are composed of y values of both traces. My two traces don’t use the same y-axis and therefore a transformation function has to be applied to one of the traces to harmonise the height values before the polygon string can be composed.

I can’t share all the code as I have also added bits where the y-axes are scaled at higher values which would add quite some complexity to the answer, but I hope that you get the idea.

Answered By: stopopol

I have a solution based on maplotlib’s fill_between:

ax = df[['rate1', 'rate2']].plot()
ax.fill_between(
    df.index, df['rate1'], df[['rate1', 'rate2']].min(1),
    color='red', alpha=0.1
);
ax.fill_between(
    df.index, df['rate1'], df[['rate1', 'rate2']].max(1),
    color='green', alpha=0.1
);
Answered By: IanS