Fill area upper to lower bound in continuous error bars

Hi everyone,

I’m trying to plot a stock price chart with upper and lower bound, and would like to fill the area in grey between
upper and lower.

I only want to fill it between upper and lower bound, with no overlaps which darkens the color.
However it seems I’m keep getting the fill parameters wrong.

I would really appreciate for advice here :slight_smile:

I’m attaching the code, and the output below.

Thank you in advance !

def plot_volatility(dataframe, filename):
    """
    Interactive plotting for volatility
    
    input: 
    dataframe: Dataframe with upperbound, lowerbound, moving average, close.
    filename: Plot is saved as html file. Assign a name for the file.
    
    output:
    Interactive plotly plot and html file
    """
    
    upper_bound = go.Scatter(
        name='Upper Bound',
        x=dataframe['date'],
        y=dataframe['upper_band'] ,
        mode='lines',
        line=dict(width=0.5,
                 color="rgb(255, 188, 0)"),
        fillcolor='rgba(68, 68, 68, 0.1)',
        fill='toself')
    
    trace1 = go.Scatter(
        name='Close',
        x=dataframe['date'],
        y=dataframe['Close'],
        mode='lines',
        line=dict(color='rgb(31, 119, 180)'),
        fillcolor='rgba(68, 68, 68, 0.2)',
        fill='tonexty')
    
    trace2 = go.Scatter(
        name='Moving Avg',
        x=dataframe['date'],
        y=dataframe['moving_avg'],
        mode='lines',
        line=dict(color='rgb(246, 23, 26)'),
        fillcolor='rgba(68, 68, 68, 0.2)',
        fill='tonexty')

    lower_bound = go.Scatter(
        name='Lower Bound',
        x=dataframe['date'],
        y=dataframe['lower_band'],
        mode='lines',
        line=dict(width=0.5, color="rgb(141, 196, 26)"),)

    data = [lower_bound, trace1, trace2, upper_bound]

    layout = go.Layout(
        yaxis=dict(title='Price'),
        title='Volatility Visualization',
        xaxis=dict(
            rangeselector=dict(
                buttons=list([
                    dict(count=1,
                         label='1m',
                         step='month',
                         stepmode='backward'),
                    dict(count=3,
                         label='3m',
                         step='month',
                         stepmode='backward'),
                    dict(count=6,
                         label='6m',
                         step='month',
                         stepmode='backward'),
                    dict(step='all')
                ])
            ),          
            rangeslider=dict(
                visible = True
            ),
            type='date'
        ),        
        showlegend = True)

    fig = go.Figure(data=data, layout=layout)
    return py.iplot(fig, filename=filename+'.html')

[Update]

Solved the problem !

I played around with the order of traces.

I changed the data;
from [lower_bound, trace1, trace2, upper_bound]
to [lower_bound, upper_bound, trace1, trace2]

and now the output shows as below.

1 Like

In case you are still interested, I created a method that handles a dataframe the same way as seaborn does it. See link below.

1 Like

That is a really impressive script, and (almost) exactly what I am looking for! If I understand correctly, your script helps to determine a standard deviation and plots them together with the original timeseries, with the option to use one grouping variable.

Do you know if it would be possible to adapt the script to supply the lower boundary/minimum and upper boundary/maximum for each timeseries from the data source (instead of calculating it) ?

Starting from the sample dataset, this would start from adding an extra column that contains something like ‘min’ / ‘likely’ (= original timeseries) / ‘max’:

Hi, I will reply directly in the post.

Someone may find this useful. I wrote a simple function to extend plotly.express.line using the same interface as plotly.express.scatter:

def line(error_y_mode=None, **kwargs):
	"""Extension of `plotly.express.line` to use error bands."""
	ERROR_MODES = {'bar','band','bars','bands',None}
	if error_y_mode not in ERROR_MODES:
		raise ValueError(f"'error_y_mode' must be one of {ERROR_MODES}, received {repr(error_y_mode)}.")
	if error_y_mode in {'bar','bars',None}:
		fig = px.line(**kwargs)
	elif error_y_mode in {'band','bands'}:
		if 'error_y' not in kwargs:
			raise ValueError(f"If you provide argument 'error_y_mode' you must also provide 'error_y'.")
		figure_with_error_bars = px.line(**kwargs)
		fig = px.line(**{arg: val for arg,val in kwargs.items() if arg != 'error_y'})
		for data in figure_with_error_bars.data:
			x = list(data['x'])
			y_upper = list(data['y'] + data['error_y']['array'])
			y_lower = list(data['y'] - data['error_y']['array'] if data['error_y']['arrayminus'] is None else data['y'] - data['error_y']['arrayminus'])
			color = f"rgba({tuple(int(data['line']['color'].lstrip('#')[i:i+2], 16) for i in (0, 2, 4))},.3)".replace('((','(').replace('),',',').replace(' ','')
			fig.add_trace(
				go.Scatter(
					x = x+x[::-1],
					y = y_upper+y_lower[::-1],
					fill = 'toself',
					fillcolor = color,
					line = dict(
						color = 'rgba(255,255,255,0)'
					),
					hoverinfo = "skip",
					showlegend = False,
					legendgroup = data['legendgroup'],
				)
			)
		# Reorder data as said here: https://stackoverflow.com/a/66854398/8849755
		reordered_data = []
		for i in range(int(len(fig.data)/2)):
			reordered_data.append(fig.data[i+int(len(fig.data)/2)])
			reordered_data.append(fig.data[i])
		fig.data = tuple(reordered_data)
	return fig

Usage example:

import plotly.express as px
import pandas

df = px.data.gapminder().query('continent=="Americas"')
df = df[df['country'].isin({'Argentina','Brazil','Colombia'})]
df['lifeExp std'] = df['lifeExp']*.1 # Invent some error data...

for error_y_mode in {'band', 'bar'}:
	fig = line(
		data_frame = df,
		x = 'year',
		y = 'lifeExp',
		error_y = 'lifeExp std',
		error_y_mode = error_y_mode,
		color = 'country',
		title = f'Using error {error_y_mode}',
		markers = '.',
	)
	fig.show()

Result:


Ignore this paragraph. This text is so it let me save the edit, I added some links. But it is complaining that it is too similar. Why the hell someone wasted his/her time implemented a system that checks how similar is an edit? If I made a typo of one letter I cannot fix it?