Cant update figure layout on callback

HI,

Basically i want to sync the scale of the timeseries graphs bidirectionly by changing the layout xaxis and yaxis ranges based of the relayoutdata of the other graph, But i cant seem to understand why my graph doesnt update the layout nor i the both graphs stops showing the ploted data

@app.callback(
    Output('timeseries-graph', 'figure',allow_duplicate=True),
    Output('timeseries-graph2', 'figure',allow_duplicate=True),
    Input('timeseries-graph', 'relayoutData'),
    Input('timeseries-graph2', 'relayoutData'),
    State('timeseries-graph', 'figure'),
    State('timeseries-graph2', 'figure'),
    prevent_initial_call=True
    
)

def sync_graphs(relayoutData1, relayoutData2, fig1, fig2):
    new1=fig1
    new2=fig2
    ctx = dash.callback_context
    print(ctx.triggered)
    
    if not ctx.triggered:
        return new1, new2
    
    if fig1 is None or fig2 is None:
        print('retornou None')
        return new1, new2

    triggered_id = ctx.triggered[0]['prop_id'].split('.')[0]

    if triggered_id == 'timeseries-graph' and relayoutData1:
        if 'xaxis.range[0]' in relayoutData1 and 'xaxis.range[1]' in relayoutData1:
            x0 = relayoutData1['xaxis.range[0]']
            x1 = relayoutData1['xaxis.range[1]']
            new2['layout']['xaxis'] = fig2['layout'].get('xaxis', {})
            new2['layout']['xaxis'] = {'range':(x0, x1),'autorange':False}

        if 'yaxis.range[0]' in relayoutData1 and 'yaxis.range[1]' in relayoutData1:
            y0 = relayoutData1['yaxis.range[0]']
            y1 = relayoutData1['yaxis.range[1]']
            new2['layout']['yaxis'] = fig2['layout'].get('yaxis', {})
            new2['layout']['yaxis'] = {'range':(y0, y1),'autorange':False}


    if triggered_id == 'timeseries-graph2' and relayoutData2:
        if 'xaxis.range[0]' in relayoutData2 and 'xaxis.range[1]' in relayoutData2:
            x0 = relayoutData2['xaxis.range[0]']
            x1 = relayoutData2['xaxis.range[1]']
            new1['layout']['xaxis'] = fig1['layout'].get('xaxis', {})
            new1['layout']['xaxis'] = {'range':(x0, x1),'autorange':False}

        if 'yaxis.range[0]' in relayoutData2 and 'yaxis.range[1]' in relayoutData2:
            y0 = relayoutData2['yaxis.range[0]']
            y1 = relayoutData2['yaxis.range[1]']
            new1['layout']['yaxis'] = fig1['layout'].get('yaxis', {})
            new1['layout']['yaxis'] = {'range':(y0, y1),'autorange':False}

    print(new1)
    print(x0,x1)
    return new1, new2

after some debugging i verified that new 1 is a valid dict with all the data point it should have and with the right ranges, but when i return it, it doesnt really show it.

here is dash layout of that part:

    dcc.Graph(id='timeseries-graph',
              style={'height': '350px'},
              config={'scrollZoom': True,'displaylogo':False},
              loading_state={'is_loading ':True}
             ),
    

    dcc.Graph(id='timeseries-graph2',
              style={'height': '350px', 'margin-top': '-40px'},
              config={'scrollZoom': True,'displaylogo':False}
             ),
    
    html.Span(id='info graph',
              children=''),
    
    html.Span(id='info graph2',
              children='')
])

Any tips for fixing it? I can provide any info if needed.

Thanks in advance!

Hi @thimta1 and welcome to the Dash community :slight_smile:

It will be easier to help if you can include a complete example that reproduces the issue. More info on how to do that here:

1 Like

So, i tried to reproduce the problem i get as simple as possible.

import socket
import dash
from dash import dcc,html,Dash
from dash.dependencies import Input, Output,State
import plotly.graph_objs as go
import plotly.express as px
import plotly.offline as pyo
from dash.exceptions import PreventUpdate

def get_local_ip():
    hostname = socket.gethostname()
    try:
        # Resolve o hostname para obter o endereƧo IP local
        local_ip = socket.gethostbyname(hostname)
    except socket.gaierror as e:
        print(f"Erro ao resolver o hostname: {e}")
        local_ip = None
    return local_ip

# iniatlize app Dash
app = dash.Dash(__name__)
app = Dash(prevent_initial_callbacks="initial_duplicate")

figures=[
    go.Figure(data=[go.Scatter(x=[1, 2, 3,4,5,6,7,8], y=[4, 1, 2,6,3,6,7,4])]),
    go.Figure(data=[go.Scatter(x=[1, 2, 3,5,7,8], y=[4, 1, 2,2,6,5])]),
    go.Figure(data=[go.Scatter(x=[1,3,4,5,7,8], y=[4, 1, 2,2,6,5])]),
    go.Figure(data=[go.Scatter(x=[1,3,4,5,6,7,8], y=[4, 1, 2,2,8,6,5])]),
]
app.layout = html.Div(children=[
    
    dcc.Dropdown(id='select 1',
                options=['1','2','3','4']
                ),
    
    dcc.Dropdown(id='select 2',
                options=['1','2','3','4']
                ),
    
    dcc.Graph(id='timeseries-graph',
              style={'height': '350px'},
              config={'scrollZoom': True,'displaylogo':False},
              loading_state={'is_loading ':True}
             ),
    

    dcc.Graph(id='timeseries-graph2',
              style={'height': '350px', 'margin-top': '-40px'},
              config={'scrollZoom': True,'displaylogo':False}
             ),
    
])

@app.callback(
    Output('timeseries-graph', 'figure',allow_duplicate=True),
    Input('select 1','value')
)

def update_graph1(value):
    if value is None:
        raise PreventUpdate
    return figures[int(value)]

@app.callback(
    Output('timeseries-graph2', 'figure',allow_duplicate=True),
    Input('select 2','value')
)

def update_graph1(value):
    if value is None:
        raise PreventUpdate
    return figures[int(value)]


@app.callback(
    Output('timeseries-graph', 'figure',allow_duplicate=True),
    Output('timeseries-graph2', 'figure',allow_duplicate=True),
    Input('timeseries-graph', 'relayoutData'),
    Input('timeseries-graph2', 'relayoutData'),
    State('timeseries-graph', 'figure'),
    State('timeseries-graph2', 'figure'),
    prevent_initial_call=True
    
)

def sync_graphs(relayoutData1, relayoutData2, fig1, fig2):
    new1=fig1
    new2=fig2
    ctx = dash.callback_context
    print(ctx.triggered)
    
    if not ctx.triggered:
        return new1, new2


    triggered_id = ctx.triggered[0]['prop_id'].split('.')[0]

    if triggered_id == 'timeseries-graph' and relayoutData1:
        if 'xaxis.range[0]' in relayoutData1 and 'xaxis.range[1]' in relayoutData1:
            x0 = relayoutData1['xaxis.range[0]']
            x1 = relayoutData1['xaxis.range[1]']
            new2['layout']['xaxis'] = fig2['layout'].get('xaxis', {})
            new2['layout']['xaxis'] = {'range':(x0, x1),'autorange':False}

        if 'yaxis.range[0]' in relayoutData1 and 'yaxis.range[1]' in relayoutData1:
            y0 = relayoutData1['yaxis.range[0]']
            y1 = relayoutData1['yaxis.range[1]']
            new2['layout']['yaxis'] = fig2['layout'].get('yaxis', {})
            new2['layout']['yaxis'] = {'range':(y0, y1),'autorange':False}


    if triggered_id == 'timeseries-graph2' and relayoutData2:
        if 'xaxis.range[0]' in relayoutData2 and 'xaxis.range[1]' in relayoutData2:
            x0 = relayoutData2['xaxis.range[0]']
            x1 = relayoutData2['xaxis.range[1]']
            new1['layout']['xaxis'] = fig1['layout'].get('xaxis', {})
            new1['layout']['xaxis'] = {'range':(x0, x1),'autorange':False}

        if 'yaxis.range[0]' in relayoutData2 and 'yaxis.range[1]' in relayoutData2:
            y0 = relayoutData2['yaxis.range[0]']
            y1 = relayoutData2['yaxis.range[1]']
            new1['layout']['yaxis'] = fig1['layout'].get('yaxis', {})
            new1['layout']['yaxis'] = {'range':(y0, y1),'autorange':False}

    return new1, new2

# run app
if __name__ == '__main__':
        local_ip = get_local_ip()
        app.run(
        debug=True,
        host = local_ip,
        port=8050)

basically, after i did this code, i noticed that it will work if i initialize the dash with a ā€˜placeholderā€™ figure in the dcc.dash so it wont be None.

But im afraid this ā€œsolutionā€ isnt stable.

Also, i noticed that if i delete the sync callback, the graphs update just fine. The same happens ifi delete the updates callbacks, the sync callback would function perfectly.

So, i guess that for this sync callback (the way i did it) to work it needs to have some graph plotted.

My only doubt right now is why wouldnt the plot even appear in exemple above? because the sync function isnt returning None, at least for 1 of those graphs. Also, is there a way to sync the scale without a graph plotted?

Thanks for the attention.

Dash version: 2.17.0
Dcc version: 2.14.0
html version: 2.0.18
Im coding on jupyter notebook, so this whole code is on a cell
Im on windows 11

Hi @thimta1

I recommend combining the callbacks rather than using duplicate outputs in this case. You can find more info in the dash docs:

Where you have multiple callbacks targeting the same output, and they both run at the same time, the order in which updates happen is not guaranteed. This may be an issue if you are updating the same part of the property from each callback. For example, a callback output that updates the entire figure and another one that updates the figure layout.

1 Like

I see, so i should merge the update layout and the update graph and use dash.callbck_context to determine what should i update?

Yes, thatā€™s correct. And there is a slightly easier way to check for which component triggered the callback:

add ctx to your imports

from dash import ctx

then in the callback you can just use ctx.triggered_id to get the id

Ok! Thanks for tip!