I have a Pandas dataframe containing 10 columns and I want to plot 3 of those columns using plotly.express
If 2 or 3 lines are plotted I want the area between them to be filled with a transparent color (rgba-value with alpha being low). If just 1 line plotted I don't want the plot to be filled to zero-axis.
My problem is that I can fill the area between the HIGH and LOW lines and it remains filled if I remove MIDDLE, but it goes wrong otherwise.
'''
# Draw aRange using a dataframe containing a, aHigh, aLow
HIGH = "rgb(255,0,0,0)"
LOW = "rgb(0,0,255)"
MIDDLE = "rgb(0,255,0)"
FILL = "rgba(255, 172, 167, 0.2)"
maxRange = df.aHigh.max() # Maximum price within aHigh
minRange = df.aLow[df.aLow>0].min() # Minimum price within aLow skipping None and zero values
extraRange = 0.05 * (maxRange - minRange) # Take 5% extra space for the range above and below
fig = go.Figure()
fig.add_trace(go.Scatter(x=df.date, y=df.aHigh, mode='lines', name='High', line_color=HIGH,
fill=None))
fig.add_trace(go.Scatter(x=df.date, y=df.aLow, mode='lines', name='Low', line_color=LOW,
fill='tonexty', fillcolor=FILL))
fig.add_trace(go.Scatter(x=df.date, y=df.a, mode='lines', name='Middle', line_color=MIDDLE,
fill=None))
fig.update_layout(title = "Ticker", xaxis_title = 'Date', yaxis_title = 'Price',
yaxis_range = [minRange - extraRange, maxRange + extraRange])
fig.update_yaxes(fixedrange=False)
fig.show()
''' The pictures show various results after hiding one of the lines. The second and thord picture show undesired results, the first and fourth picture are OK.
How can I achieve what I want?
Note that this is the expected behavior :
fill
– [...] "tonextx" and "tonexty" fill between the endpoints of this trace and the endpoints of the trace before it [...]. If there is no trace before it, they behave like "tozerox" and "tozeroy".
The "trace before it" here means any visible trace with a lower index (trace indices are zero-based and correspond to the order of creation). So when you toggle a trace visibility, the fill attribute should be updated accordingly (ie. the first visible trace should have no fill), which you could do only if you override the default behavior of legend clicks.
Since interactive renderers display figures using Plotly.js, you can actually use JavaScript+Plotly.js to hook into the plotly_legendclick
event and override the default behavior. To achieve this, you just need to pass the javascript code as a string to plotly's Figure.show()
method via the post_script
parameter :
fig = go.Figure()
fig.add_trace(go.Scatter(x=df.date, y=df.aHigh, mode='lines', name='High',
line_color=HIGH, fill=None)) # trace 0
fig.add_trace(go.Scatter(x=df.date, y=df.a, mode='lines', name='Middle',
line_color=MIDDLE, fill='tonexty', fillcolor=FILL)) # trace 1
fig.add_trace(go.Scatter(x=df.date, y=df.aLow, mode='lines', name='Low',
line_color=LOW, fill='tonexty', fillcolor=FILL)) # trace 2
js = '''
const gd = document.getElementById('{plot_id}');
gd.on('plotly_legendclick', function(eventData) {
const i = eventData.curveNumber;
const traces = eventData.fullData;
traces[i].visible = traces[i].visible === true ? 'legendonly' : true;
const visible = traces.map(trace => trace.visible);
// The first visible trace + all traces before it should have no fill.
const iFirst = visible.findIndex(v => v === true);
const fill = traces.map((trace, j) => j <= iFirst ? null : 'tonexty');
Plotly.restyle(gd, {visible, fill});
return false;
});
'''
fig.update_layout(title="Ticker", xaxis_title='Date', yaxis_title='Price')
fig.update_yaxes(fixedrange=False, range=[minRange-extraRange, maxRange+extraRange])
fig.show(post_script=[js])