Need to control Y axis range with mouse

Hi there,
I am new to DASH and plotly. I need to control Y axis range with mouse, like you can do on tradingview app. For this I have tried to use slider in my candlestick chart but it does not work. Any alternative solution/suggestion is much appreciated. Thanks so much.

relevant scripts below -

app.layout = html.Div([
    html.H4('Candlestick chart with selectable hover mode'),
    html.P("Select hovermode:"),
    dcc.RadioItems(
        id='hovermode',
        inline=True,
        options=['x', 'x unified', 'closest'],
        value='x'
    ),
    html.Div([
        html.Div([
            dcc.Dropdown(
                id='ticker-dropdown',
                options=[{'label': ticker, 'value': ticker} for ticker in tickers],
                value=tickers[0],  # Default value could be the first ticker in the list
                searchable=True,
                style={'width': '60%', 'padding': '3px', 'color': 'black', 'margin-left': '2%'}
            ),
            dash_table.DataTable(
                id='ticker-table',
                columns=[{'name': col, 'id': col} for col in df.columns],
                page_size=100,
                data=df.to_dict('records'),
                fixed_rows={'headers': True},
                row_selectable='single',
                sort_action='native',
                style_table={'height': '900px', 'overflowY': 'auto'},
                style_header={
                    'backgroundColor': 'rgb(10, 10, 10)',
                    'fontWeight': 'bold',
                    'color': 'lightblue',
                    'textAlign': 'center'
                },
                style_cell={
                    'backgroundColor': 'rgb(30, 30, 30)',
                    'color': 'lightgrey',
                    'border': '1px solid black',
                    'minWidth': '150px', 'maxWidth': '200px', 'width': '150px'
                },
                style_data_conditional=[
                    {'if': {'row_index': 'odd'}, 'backgroundColor': 'rgb(20, 20, 20)'},
                    {'if': {'state': 'selected'}, 'backgroundColor': 'rgba(0, 116, 217, 0.3)', 'border': '1px solid blue'},
                    {
                        'if': {'column_id': '% Change', 'filter_query': '{% Change} > 0'},
                        'color': 'green'
                    },
                    {
                        'if': {'column_id': '% Change', 'filter_query': '{% Change} < 0'},
                        'color': 'red'
                    }
                ]
            ),
        ], id='ticker-table-container', style={'width': '25%', 'overflow': 'auto', 'display': 'inline-block', 'margin-left': '2%', 'verticalAlign': 'top'}),
        html.Div([
            dcc.Graph(id='candlestick-chart', style={'height': '1200px', 'width': '85%', 'display': 'inline-block'},
                      config={'scrollZoom': True}),
            dcc.Slider(
                id='yaxis-slider',
                min=0,
                max=100,
                step=1,
                value=50,
                marks=None,
                tooltip={"placement": "right", "always_visible": True},
                vertical=True,
                verticalHeight=900  # Adjust the height to match the graph
            )
        ], style={'width': '100%', 'display': 'flex', 'align-items': 'center', 'justify-content': 'space-between'})
    ], style={'display': 'flex', 'align-items': 'flex-start'}),
    dcc.Interval(id='candlestick-update-interval', interval=5 * 1000, n_intervals=0),
    dcc.Interval(id='ticker-update-interval', interval=10 * 1000, n_intervals=0),
    dcc.Store(id='initial-load', data={'loaded': False}),
    dcc.Store(id='active-ticker-store', data={'ticker': 'HINDCOPPER'}),
    html.Div(id='app-launch-trigger', style={'display': 'none'})
])






@app.callback(
    [Output('candlestick-chart', 'figure'),
     Output('yaxis-slider', 'min'),
     Output('yaxis-slider', 'max'),
     Output('yaxis-slider', 'value')],
    [Input('active-ticker-store', 'data'),
     Input('candlestick-update-interval', 'n_intervals'),
     Input('hovermode', 'value'),
     Input('yaxis-slider', 'value'),
     Input('candlestick-chart', 'relayoutData')],
    [State('candlestick-chart', 'figure')]
)
def update_candlestick_chart(active_ticker_data, n_intervals, hovermode, yaxis_slider_value, relayoutData, figure):
    ticker = active_ticker_data['ticker']
    df, _, dt_breaks = get_hist(ticker)  # Fetch historical data along with date breaks

    if df.empty:
        return go.Figure(), 0, 100, 50

    y_min = df['buy-sell_EMA_low'].min()
    y_max = df['buy-sell_EMA_high'].max()
    y_range = y_max - y_min

    # Create the candlestick chart
    fig = go.Figure(data=[go.Candlestick(
        x=df.index,
        open=df['buy-sell_EMA_open'],
        high=df['buy-sell_EMA_high'],
        low=df['buy-sell_EMA_low'],
        close=df['buy-sell_EMA_close'],
        increasing_line_color='green', 
        decreasing_line_color='red'
    )])

    # Apply the relayout data if it exists and ticker has not changed
    if relayoutData and figure and figure['layout']['uirevision'] == ticker:
        if 'xaxis.range[0]' in relayoutData:
            fig.update_layout(xaxis_range=[relayoutData['xaxis.range[0]'], relayoutData['xaxis.range[1]']])
        if 'yaxis.range[0]' in relayoutData:
            fig.update_layout(yaxis_range=[relayoutData['yaxis.range[0]'], relayoutData['yaxis.range[1]']])
    else:
        # Reset y-axis range if ticker changes
        fig.update_layout(yaxis_range=[y_min, y_max])

    # Adjust y-axis range based on slider value
    if yaxis_slider_value:
        fig.update_layout(yaxis_range=[y_min, yaxis_slider_value])

    # Customize layout to include crosshairs with labels at the end
    fig.update_layout(
        title=dict(text=f'{ticker}: Candlestick Chart', font=dict(size=30)),
        xaxis_title=dict(text='Time', font=dict(size=30)),
        yaxis_title=dict(text='Price', font=dict(size=30)),
        xaxis_rangeslider_visible=False,
        hovermode=hovermode,  # Dynamic hovermode based on radio button selection
        xaxis=dict(
            type='date',
            title_font=dict(size=30),
            rangebreaks=[
                dict(bounds=["sat", "mon"]),  # hide weekends
                dict(bounds=[16, 9], pattern="hour"),  # hide hours outside of 9.30am-4pm
                dict(values=["2019-12-25", "2020-12-24"])  # hide holidays
            ],
            showspikes=True,
            spikemode='across',
            spikesnap='cursor',
            spikedash='solid',
            spikethickness=2,
            showline=True,
            showgrid=True,
        ),
        yaxis=dict(
            title_font=dict(size=30),
            autorange=False,
            range=[y_min, yaxis_slider_value],  # Use slider value for range
            showspikes=True,
            spikemode='across',
            spikesnap='cursor',
            spikedash='solid',
            spikethickness=2,
            showline=True,
            showgrid=True,
        ),
        plot_bgcolor='black',
        paper_bgcolor='black',
        font_color='white',
        font=dict(
            family="Arial, sans-serif",
            size=30,
            color="white"
        ),
        hoverlabel=dict(
            bgcolor="white",
            font_size=16,
            font_family="Arial"
        ),
        dragmode='pan',        
        uirevision=ticker,  # Ensure the user interactions persist across updates
    )

    # Update the y-axis slider dynamically
    slider_min = y_min
    slider_max = y_max
    slider_value = y_max  # Set slider value to the max of the y range

    return fig, slider_min, slider_max, slider_value


Hi,

Sorry, your example isn’t self-contained enough for me to run it, and I can’t immediately see what might be wrong. The only thing that strikes me is that there’s a lot to be said for explicitly taking account of which input has triggered the callback in the program logic, this sort of thing:

from dash import ctx
...
trigger = ctx.triggered_id
...
if (trigger == "yaxis-slider"):
    ...

It is definitely possible to control the Y axis range with a slider in Dash, though.

If you’re able to step through the callback code line by line in a debugger does that give you any insight?

Thanks for your reply. I will do some more testing.

Can you let me know if controlling Y axis is possible without using sliders. Like , if you are familiar with the tradingview app, its very easy to just roll the mouse wheel over the Y axis(or click and drag) and it contracts/increases with ease.

Thanks

Out of the box, there is

config={'scrollZoom': True}

which zooms on both axes using the mouse wheel. I’m fairly sure it’s possible to freeze the X-axis so that mouse-wheel zoom only happens on Y, but I think then the X axis becomes entirely fixed.

If that doesn’t do what you want, then If it’s possible to set up a JS eventhandler for the actions you want to use, then you should be able to forward those events to a Dash callback - regret I don’t know the details of how to do this.

Thanks I already use

config={‘scrollZoom’: True}

this zooms both the axes together which is not desirable every time especially with candlestick charts spanning multiple days for a stock. If I could just get Y axis independently along with this scrollzoom that would solve a big problem.
Thanks again for your help

Try adding
fig.update_xaxes(fixedrange=True)