Callback on graph slider change - which property to use as Input()?

I have a python dash app that displays a graph containing two subplots and a slider.
The code is something like this:

figure = make_subplots(rows=2,cols=1, shared_xaxes=True)
figure_update_layout(xaxis=go.layout.XAxis(rangeslider=dict(autorange=True))
...
html.Div([
  dcc.Graph(id='my-graph', figure=figure),
  html.Div([dcc.Table([some_sophisticated_data])], id='my-table')
])

Now I’d like to update the table contents whenever the user moves the slider controls.

Function would be like this:

@app.callback(Output('my-table', 'children'), Input('my-graph', '???')
def renderTable(new_range):
  return dcc.Table([some_sophisticated_data])]

where some_sophisticated_data depends on the range.

What do I have to use in place of the question marks? I tried ‘xaxis.range’, but that does not fire…

Or do I have to put the slider outside the graph? This would be painful as I’d have to update the graph from the server side which would slow down the web performance (>4000 data points…)

Welcome to the dash forum, and thank you for a good description of you problem. You could make it even better by providing a small standalone example demonstrating the issue :slight_smile:

Now, to my knowledge, it is not possible to subscribe to events of the underlying properties of Plotly objects (which seems to be what you are trying to do), but please correct me if i am wrong. Hence to achieve the desired behavior, you would need “something else” to trigger the update. In Dash, that could be an interval component,

import dash
import dash_core_components as dcc
import dash_html_components as html
import plotly.graph_objects as go

from dash.dependencies import Output, Input, State
from plotly.subplots import make_subplots

figure = make_subplots(rows=2, cols=1, shared_xaxes=True)
figure.update_layout(xaxis=go.layout.XAxis(rangeslider=dict(autorange=True)))

app = dash.Dash(__name__)
app.layout = html.Div([
    dcc.Graph(id='my-graph', figure=figure),
    dcc.Interval(id='my-interval'),
    html.Div(id='my-div')
])


@app.callback(Output('my-div', 'children'),
              [Input('my-graph', "figure"), Input('my-interval', 'n_intervals')],
              [State('my-div', 'children')])
def update(figure, n_intervals, x_range_state_str):
    if figure is None or n_intervals is None:
        return dash.no_update
    # Check if update is needed.
    x_range_value = figure['layout']['xaxis']['range']
    x_range_value_str = ("({:3f},{:3f})").format(x_range_value[0], x_range_value[1])
    if x_range_value_str == x_range_state_str:
        return dash.no_update
    # Do the update.
    return x_range_value_str


if __name__ == '__main__':
    app.run_server()

While the above solution works, an external slider would probably be preferable performancewise (since you avoid the periodic requests invoked by the Interval component).

I managed to find an appropriate property to monitor for the slider: the ‘relayoutData’ event is fired when the slider is moved.
Unfortunately, there seems to be a bug in the event data creation: the data slightly differs between the event fired from moving the graph and moving the handles (or the area) in the rangeslider. So I had to take an additional step to interpret the data in both cases.

Current implementation is

@app.callback(Output('my-table', 'children'),
            [Input('my-graph', 'relayoutData')])
def render_bot_summary(relayoutData=dict()):
    if 'summary' not in cached_data.keys():
        return html.H4(str(cached_data.keys()), style=dict(color='red'))

    result = cached_data['result']
    summary = cached_data['summary']

    firstAsset, secondAsset = (summary['A1'], summary['A2'])

    data = cached_data['result']

    left = result.index[0]
    right = result.index[-1]

    if isinstance(relayoutData, dict):
        if 'xaxis.range' in relayoutData.keys():
            left, right = relayoutData['xaxis.range']
        if 'xaxis.range[0]' in relayoutData.keys():
            left = relayoutData['xaxis.range[0]']
        if 'xaxis.range[1]' in relayoutData.keys():
            right = relayoutData['xaxis.range[1]']

    left, right = pd.to_datetime(left).round('D'), pd.to_datetime(right).round('D')

    df = data[left:right]

I could have used your approach using the figure property as an additional input, that would have saved me some details.

BTW: It seems that it’s really difficult to tie an arbitrary number of graphs to the slider funcitonality: I am using two subplots, but the slider only acts on the first, although the figure has the property shared_xaxes set to True. Let’s see how it works when I separate slider and graph (huh, never dun that before…)

I think the syntax has changed since this was posted(?) In hopes this helps someone else a few hours that I spent, I revised the above slightly and got this to work:

app.layout = html.Div(
    id="app-container",
    children=[
                                 ....
                                 dcc.Store(id='duration_range'),
                                 ....
                    ]
)



@app.callback(
    Output('duration_range', 'data'),
    [Input('duration_histogram', 'relayoutData')])
def display_relayout_data(relIn=dict()):
    if isinstance(relIn, dict):
        if 'xaxis.range' in relIn.keys():
            left, right = relIn["xaxis.range"][0], relIn["xaxis.range"][1]
        else: left, right=None, None
    else: left, right=None, None
    return json.dumps([left, right])
1 Like