Plotly - Autorescaling y axis range when range slider used
Asked Answered
R

2

9

After a long search, I could not find any thread/discussion helping me to make autoscaling work with plotly.

The idea would be that when I use the x-range slider to go through the data, the y-axis would be dynamically rescaled each time I move the slider.

Currently, this is how it looks like (pretty unuseful): enter image description here

Here is a snippet of my code:

fig = plt.figure()
    
    # Layout format and buttons
    layout = dict(
        xaxis=dict(
            rangeselector=dict(
                buttons=list([
                    dict(count=1,
                         label='1m',
                         step='month',
                         stepmode='backward'),
                    dict(count=6,
                         label='6m',
                         step='month',
                         stepmode='backward'),
                    dict(count=1,
                        label='YTD',
                        step='year',
                        stepmode='todate'),
                    dict(count=1,
                        label='1y',
                        step='year',
                        stepmode='backward'),
                    dict(step='all')
                ])
            ),
            rangeslider=dict(
                visible = True
            ),
            type='date'
        )
    )
    
    # Plot open, high, low, close for each coins
    for c in dic.keys():
        ohlc = dic[c][['open','high','low','close']].copy()
        
        candlestick = go.Candlestick(x = ohlc.index,
                                     open = ohlc['open'],
                                     high = ohlc['high'],
                                     low = ohlc['low'],
                                     close = ohlc['close'],
                                     name = 'OHLC')
        
        fig = go.Figure(data = [candlestick],
                        layout = layout)
        
        fig.update_yaxes(showgrid=True, 
                          zeroline=False, 
                          showticklabels=True,
                          showspikes=True, 
                          spikemode='across', 
                          spikesnap='cursor', 
                          spikethickness=1, 
                          showline=True, 
                          spikedash='dot',
                          spikecolor="#707070",
                          autorange = True,     # DOESN'T WORK....
                          fixedrange= False     # DOESN'T WORK....
                          )
            
        fig.update_xaxes(showgrid=True, 
                          zeroline=False, 
                          showticklabels=True,
                          showspikes=True, 
                          spikemode='across', 
                          spikesnap='cursor', 
                          spikethickness=1, 
                          showline=True, 
                          spikedash='dot',
                          spikecolor="#707070",
                          type='date',
                          )
        
        fig.update_layout(title = 'OHLC for: '+c,
                          height = 750,
                          plot_bgcolor="#FFFFFF",
                          hovermode="x",
                          hoverdistance=100,
                          spikedistance=1000,
                          xaxis_rangeslider_visible=True,
                          dragmode='pan',
                          )
        
        fig.show()

I tried to use autorange = True and fixedrange = False but apparently that does nothing for reasons I don't understand.

Rhine answered 29/3, 2023 at 11:12 Comment(9)
As far as I know the only of achieving this behavior is using Dash. The furthest you can get with Plotly only is to be able to manually zoom in and scale the y axis that way Are you open to use Dash? github.com/plotly/plotly.py/issues/932Iene
@Rhine yes to what @Brener Ramos wrote. if you would be interested in a Plotly Dash based solution, I would be happy to write one up for youDisaffirm
Hi all, sure I would be interested in any solution. Because right now its litterally unusable.... I never used Plotly Dash, is the structure completely different ?Rhine
Was any progress made with this? I am having the same issues now.Icaria
Ditto. Passing in a min, max into the range doesn't work because the range slider isn't within a callback so the limits don't get updated.Britska
@DerekO, did you get around to creating a fix in dash plotly?Britska
@Britska oh i completely forgot about this – but happy to circle back on this when i have a momentDisaffirm
SO's past responses from plotly developers. Please refer to this response.Aharon
Yes @r-beginners, it appears a plotly fix is not available but a work around may be achieved with dash.Britska
S
5

Plotly.py's interactive renderers rely on Plotly.js so we can hook into the plotly_relayout event and rescale the yaxis range when needed by passing the proper javascript code to the method go.Figure.show(), thanks to the post_script parameter. I provide here a JS/Plotly.js solution, and then the Python solution as it consists of embedding the JS code.

Note you might need to adapt the formatDateRange() function (in both cases) according to your actual OHLC data resolution (see comment).

Plotly.js :

Plotly.newPlot('<plot_id>', data, layout).then(gd => {

  gd.on('plotly_relayout', (e) => {
    if (
      !e || e.autosize || e.width || e.height ||  // plot init or resizing
      e['yaxis.range'] || e['yaxis.autorange'] || // yrange already adjusted
      e['yaxis.range[0]'] || e['yaxis.range[1]']  // yrange manually set
    ) {
      // Nothing to do.
      return dash_clientside.no_update;
    }

    if (e['xaxis.autorange']) {
      Plotly.relayout(gd, {'yaxis.autorange': true});
      return;
    }

    // NB. `formatDateRange()` depends on your OHLC data resolution (assuming one 
    // index per day here, formatted as 'yyyy-MM-dd'; but could be per hour, etc.) :
    // - Slider/zoom range uses datetime format 'yyyy-MM-dd hh:mm:ss.SSSS'
    // - need to match the date format 'yyyy-MM-dd'
    const formatDateRange = x => x.replace(/(\d{4}-\d{2}-\d{2}).*/, '$1');
    const xrange = gd._fullLayout.xaxis.range.map(formatDateRange);

    const ohlc = gd._fullData[0];
    let i0 = ohlc.x.indexOf(xrange[0]);
    let i1 = ohlc.x.indexOf(xrange[1]);

    if (i0 === -1) i0 = 0;
    if (i1 === -1) i1 = ohlc.x.length - 1;

    const data = [...ohlc.open.slice(i0, i1), ...ohlc.close.slice(i0, i1)];
    const ymin = Math.min(...data);
    const ymax = Math.max(...data);

    const room = Math.floor((ymax - ymin) / 20);
    const yrange = [ymin - room, ymax + room];

    Plotly.relayout(gd, {'yaxis.range': yrange});
  });
});

Plotly.py :

We use the placeholder '{plot_id}' for identifying the graph div whose id is generated in this case. The javascript snippet(s) are executed just after plot creation.

# ... build `fig`

fig.update_layout(
    xaxis_rangeslider_visible=True,
    xaxis_rangeslider_yaxis_rangemode="auto" # so we can still see the big picture in the rangeslider plot
)

js = '''
const gd = document.getElementById('{plot_id}');

gd.on('plotly_relayout', (e) => {
    if (
      !e || e.autosize || e.width || e.height || 
      e['yaxis.range'] || e['yaxis.autorange'] ||
      e['yaxis.range[0]'] || e['yaxis.range[1]'] 
    ) {
        return dash_clientside.no_update;
    }

    if (e['xaxis.autorange']) {
        Plotly.relayout(gd, {'yaxis.autorange': true});
        return;
    }

    const formatDateRange = x => x.replace(/(\d{4}-\d{2}-\d{2}).*/, '$1');
    const xrange = gd._fullLayout.xaxis.range.map(formatDateRange);

    const ohlc = gd._fullData[0];
    let i0 = ohlc.x.indexOf(xrange[0]);
    let i1 = ohlc.x.indexOf(xrange[1]);

    if (i0 === -1) i0 = 0;
    if (i1 === -1) i1 = ohlc.x.length - 1;

    const data = [...ohlc.open.slice(i0, i1), ...ohlc.close.slice(i0, i1)];
    const ymin = Math.min(...data);
    const ymax = Math.max(...data);

    const room = Math.floor((ymax - ymin) / 20);
    const yrange = [ymin - room, ymax + room];

    Plotly.relayout(gd, {'yaxis.range': yrange});
});
'''

fig.show(post_script=[js])

output-gif

Suzette answered 12/4, 2024 at 18:19 Comment(0)
R
0

The python solution provided by @EricLavault doesn't seem to be working for autoscaling y-axis: enter image description here

def plot_signals(data, trades_df, title='Buy and Sell Signals'):
"""
Plots interactive candlestick chart with buy and sell signals using Plotly.

Parameters:
    data (pd.DataFrame): DataFrame containing OHLC data and columns 'entry_dt' and 'exit_dt'.
    trades_df (pd.DataFrame): DataFrame containing trade information with 'entry_dt' and 'exit_dt'.
    title (str): Title of the plot.
"""
# Ensure 'Date' is in datetime format
data['Date'] = pd.to_datetime(data['Date'])

# Ensure trades_df dates are in datetime format
trades_df['entry_dt'] = pd.to_datetime(trades_df['entry_dt'])
trades_df['exit_dt'] = pd.to_datetime(trades_df['exit_dt'])

# Add entry and exit dates to the data DataFrame
data['entry_dt'] = data['Date'].map(lambda date: trades_df.loc[trades_df['entry_dt'] == date, 'entry_dt'].values[0] if not trades_df[trades_df['entry_dt'] == date].empty else None)
data['exit_dt'] = data['Date'].map(lambda date: trades_df.loc[trades_df['exit_dt'] == date, 'exit_dt'].values[0] if not trades_df[trades_df['exit_dt'] == date].empty else None)

# Create candlestick figure
fig = go.Figure(data=[go.Candlestick(
    x=data['Date'],
    open=data['Open'],
    high=data['High'],
    low=data['Low'],
    close=data['Close'],
    name='Candlesticks'
)])

# Add buy signals
buy_signals = data[data['entry_dt'].notna()]
fig.add_trace(go.Scatter(
    x=buy_signals['Date'],
    y=buy_signals['Low'] * 0.995,
    mode='markers',
    marker=dict(symbol='triangle-up', size=12, color='green'),
    name='Buy Signal'
))

# Add sell signals
sell_signals = data[data['exit_dt'].notna()]
fig.add_trace(go.Scatter(
    x=sell_signals['Date'],
    y=sell_signals['High'] * 1.005,
    mode='markers',
    marker=dict(symbol='triangle-down', size=12, color='red'),
    name='Sell Signal'
))

# Update layout for better visualization
fig.update_layout(
    title=title,
    xaxis_title='Date',
    yaxis_title='Price',
    xaxis=dict(type='category'),  # Treats x-axis as categorical to remove gaps
    # yaxis=dict(autorange=True),   # Enable automatic y-axis scaling
    xaxis_rangeslider_visible=True,
    xaxis_rangeslider_yaxis_rangemode="auto",
    template='plotly_white'
)

js = '''
const gd = document.getElementById('{plot_id}');

gd.on('plotly_relayout', (e) => {
    if (
      !e || e.autosize || e.width || e.height ||
      e['yaxis.range'] || e['yaxis.autorange'] ||
      e['yaxis.range[0]'] || e['yaxis.range[1]']
    ) {
        return dash_clientside.no_update;
    }

    if (e['xaxis.autorange']) {
        Plotly.relayout(gd, {'yaxis.autorange': true});
        return;
    }

    const formatDateRange = x => x.replace(/(\d{4}-\d{2}-\d{2}).*/, '$1');
    const xrange = gd._fullLayout.xaxis.range.map(formatDateRange);

    const ohlc = gd._fullData[0];
    let i0 = ohlc.x.indexOf(xrange[0]);
    let i1 = ohlc.x.indexOf(xrange[1]);

    if (i0 === -1) i0 = 0;
    if (i1 === -1) i1 = ohlc.x.length - 1;

    const data = [...ohlc.open.slice(i0, i1), ...ohlc.close.slice(i0, i1)];
    const ymin = Math.min(...data);
    const ymax = Math.max(...data);

    const room = Math.floor((ymax - ymin) / 20);
    const yrange = [ymin - room, ymax + room];

    Plotly.relayout(gd, {'yaxis.range': yrange});
});
'''

# Show the plot
fig.show(post_script=[js])
Reopen answered 23/8, 2024 at 19:21 Comment(1)
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.Daughterly

© 2022 - 2025 — McMap. All rights reserved.