Triggering callback function and updating all connected clients

Hi, We have a requirement to display a dash page on a big-screen monitor, and mobile device simultaneously.
When the user interacts with the mobile page (for example modifies a dropdown) and it triggers the callback to update the HTML, It needs to also do the same thing on the big-screen.

Because the are unique client sessions connected to the server, Client A does not update its page when Client B changes theirs. Is there any recommended solution to this problem?

1 Like

To achieve this behavior, you could

  • Save all state on the server, e.g. in a file or a redis cache
  • Add an interval component to the page, which pulls state changes at a regular interval, e.g. once per second, and updates the client page(s) accordingly
1 Like

Hi Emil, Thanks for the reply.

I understand the process of triggering callbacks with intervals and updating the state of the component. However I am not sure what you mean when you refer to using a file or redis cache.

In this case, There are LED icons, when the user clicks on one, it updates the histogram graph (displays LED name and past activity). The function definition looks like this

@app.callback(
    [
        Output("histogram-graph", "figure"),
        Output("store-data-config", "data"),
        Output("histogram-title", "children"),
    ],
    [
        Input("interval", "n_intervals"),
        Input("SatStore", "data"),
        Input("control-panel-toggle-minute", "value"),
        Input("control-panel-elevation", "n_clicks"),
        Input("control-panel-temperature", "n_clicks"),
        Input("control-panel-speed", "n_clicks"),
        Input("control-panel-latitude", "n_clicks"),
        Input("control-panel-longitude", "n_clicks"),
        Input("control-panel-fuel", "n_clicks"),
        Input("control-panel-battery", "n_clicks"),
    ],
    [
        State("store-data", "data"),
        State("store-data-config", "data"),
        State("histogram-graph", "figure"),
        State("store-data-config", "data"),
        State("histogram-title", "children"),
        State("SatStore", "data"),
    ],
)
def update_graph(
    interval,
    satellite_type,
    minute_mode,
    elevation_n_clicks,
    temperature_n_clicks,
    speed_n_clicks,
    latitude_n_clicks,
    longitude_n_clicks,
    fuel_n_clicks,
    battery_n_clicks,
    data,
    data_config,
    old_figure,
    old_data,
    old_title,
    sat_type2,
):

    new_data_config = data_config
    info_type = data_config["info_type"]
    ctx = dash.callback_context
    #print('Graph Update Triggered')

    # Check which input fired off the component
    if not ctx.triggered:
        trigger_input = ""
    else:
        trigger_input = ctx.triggered[0]["prop_id"].split(".")[0]

...

elif trigger_input == "control-panel-speed":
        set_y_range("speed")
        info_type, new_title = update_graph_data("speed")

...

    return [figure, new_data_config, new_title]


I can save the return data when the callback is fired, and send it to all connected clients. But where do I save it?

Hi, I managed to store the state in a global variable and update the callback, so it replicates across clients. However, each client is updating at slightly different times, and so there is a desync between the mobile and big-screen versions of the page.

Is there some way to serve the same instance of the page to multiple clients?

It is generally not recommended to use global variables. To use a redis cache, you must first setup redis,
and you can then send the data to redis for in-memory storage (server side). To store the data in a file, just write it to some temp file. Something along the lines of,

import os
import json
import tempfile

data_file = os.path.join(tempfile.gettempdir(), "some_data")


@app.callback(Output('dummy', 'children'),
              [Input('...', '...')])
def update_data(*args):
    # Do any processing needed. The result should be json serializable for this code to work.
    data = process_data(*args)
    # Write the data to a file (server side).
    with open(data_file, 'w') as f:
        json.dump(data, f)


@app.callback([Output("histogram-graph", "figure"),
               Output("store-data-config", "data"),
               Output("histogram-title", "children")],
              [Input('interval', 'n_intervals')])
def update_graph(n_intervals):
    # Read the data from the file (server side).
    with open(data_file, 'r') as f:
        data = json.load(f)
    # Create the graphs.
    return create_figure_and_stuff_from_data(data)

I guess that the redis option would be most appropriate, but the file option is simpler, and it might be good enough for your use case. Note that with the proposed solution, the clients can be out of sync with a time delta up to interval of the Interval component.

Hi Emil,
Thanks for the response, I will doing something like the code you posted. From the looks of it, it isnt possible to display the same instance of the page across clients, because they are generated client-side from the html. Is it possible to synchronize the interval clocks, and correct if the deltas diverge too much?

looks like I will have to add the interval input to all the callbacks, So the general approach is something like this:

  • Any client triggers a callback from an input (click, container update etc)
  • Data is processed as usual, return value is passed to outputs (updates local html), and also saved to a server-side redis/file
  • Interval input is ticking and loads and returns the data it just saved out, and to all other clients

Is this the general approach? I have been having some issues because some of the callbacks already have an interval input, and it seems like the graph was not updating because they would have some kind of race condition

Each client sees their own instance of the application, so you need to synchronize them “yourself”. With the Interval component approach, i guess you would be able to synchronize the clients within a delta of around 1 second (depending on the network connection, server hardware, etc.). If that is not good enough, it might be necessary to move to another approach, e.g. sockets.

With the solution that i proposed previously, the data flow would be,

  1. Any client triggers a callback from an input. The result is written to a file, say some_data. For simplicity, you could group all inputs into a single callback (this might improve/degrade the performance of the app depending on the current callback structure).
  2. Each client reads the file some_data once per second (or so) and updates the graphs accordingly. To keep the client state consistent, you might also need to update the input controls.

Hence in the callback (1), you would have no outputs (hence the dummy id in the example) and all inputs. In the callback (2) you would have the Interval component as input and all graphs and controls as outputs. If you have many controls and/or graphs or you do some heavy calculations, you might need to do some performance optimizations.

Hey, I managed to get it working, Using something similar to what is shown below. This triggers the client callbacks normally, and is polling the data_file to load any parts which were changed by another client. Thanks for the help

import os
import json
import tempfile
data_file = os.path.join(tempfile.gettempdir(), "some_data")


...

@app.callback(
     Output('SatStore', 'data'),
    [
        Input('satellite-dropdown-component', 'value'),
    ])
def update_store(value):
    
    satdata = value
    if value != None:
        with open(data_file, 'w') as f:
            json.dump(satdata, f)
    
    #return dash.no_update
    return value

...


@app.callback(
    [
        Output("histogram-graph", "figure"),
        Output("store-data-config", "data"),
        Output("histogram-title", "children"),

    ],
    [
        Input("interval", "n_intervals"),
        #Input("satellite-dropdown-component", "value"),
        Input("SatStore", "data"),
        Input("control-panel-toggle-minute", "value"),
        Input("control-panel-elevation", "n_clicks"),
        Input("control-panel-temperature", "n_clicks"),
        Input("control-panel-speed", "n_clicks"),
        Input("control-panel-latitude", "n_clicks"),
        Input("control-panel-longitude", "n_clicks"),
        Input("control-panel-fuel", "n_clicks"),
        Input("control-panel-battery", "n_clicks"),
    
    ],
    [
        State("store-data", "data"),
        State("store-data-config", "data"),
        State("histogram-graph", "figure"),
        State("store-data-config", "data"),
        State("histogram-title", "children"),
        State("SatStore", "data"),
    ],
)
def update_graph(
    interval,
    satellite_type,
    minute_mode,
    elevation_n_clicks,
    temperature_n_clicks,
    speed_n_clicks,
    latitude_n_clicks,
    longitude_n_clicks,
    fuel_n_clicks,
    battery_n_clicks,
    data,
    data_config,
    old_figure,
    old_data,
    old_title,
    sat_type2,
):
    new_data_config = data_config
    info_type = data_config["info_type"]
    ctx = dash.callback_context
    
    
   

    with open(data_file, 'r') as f:
        satdata = json.load(f)
    print(satdata)
    satellite_type = satdata