Search code examples
pythonplotlyplotly-express

Python Plotly Express: How to conditionally fill an area plot?


I want to plot a time-series area plot, where positive (>= 0) values are filled in one colour and negative (< 0) values are filled in another.

Taking this example:

import pandas as pd
import numpy as np
import plotly.express as px

df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/finance-charts-apple.csv').assign(
    PnL = lambda x: x['AAPL.Close'] - 100
)
px.area(
    data_frame = df,
    x = 'Date',
    y = 'PnL',
    width = 500,
    height = 300
)

figure_1

I want the parts where PnL goes below 0 to be filled red.

So this is what I tried:

import pandas as pd
import numpy as np
import plotly.express as px

df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/finance-charts-apple.csv').assign(
    PnL = lambda x: x['AAPL.Close'] - 100
)
df['sign'] = np.where(df['PnL'] >= 0, 'positive', 'negative')
px.area(
    data_frame = df,
    x = 'Date',
    y = 'PnL',
    color = 'sign',
    color_discrete_map = {
        'positive': 'steelblue',
        'negative': 'crimson'
    },
    width = 500,
    height = 300
)

But this gives me:

figure_2

Which is not exactly what I'm looking for. What's the best way to do this?


Solution

  • This is the best I could do in the time I had patience for it:

    import plotly.graph_objects as go
    
    mask = df['PnL'] >= 0
    df['PnL_above'] = np.where(mask, df['PnL'], 0)
    df['PnL_below'] = np.where(mask, 0, df['PnL'])
    
    fig = go.Figure()
    fig.add_trace(go.Scatter(x=df['Date'], y=df['PnL_above'], fill='tozeroy'))
    fig.add_trace(go.Scatter(x=df['Date'], y=df['PnL_below'], fill='tozeroy'))
    

    Result:

    Obviously not ideal, but gets you most of the way there. There are some slight artifacts where the two traces meet, and obviously the line color is still visible when the value is zero.

    By adding mode='none' to the two traces, you can remove the line and only render the filled area: