Conditional callback using a javascript workaround

In my particular use-case, I have encountered some unforeseen problems. Using client-side callbacks I have managed to achieve the desired behaviour by simulating a “conditional callback”. I have seen other people requesting such a feature so this might save someone some time in the future. In addition, there’s probably a cleaner method to become the same results, so suggestions are very welcome.

Desired behaviour: the user can choose from a dataset which data to display. Meanwhile, the server is polled and if there is new data, the display should be automatically updated.

My initial problem: A user can select data to be shown, triggering callback A. At a regular interval, callback B polls for new data. By passing this data to A it is displayed. Since both the user and the polling call A this caused some issues. Initially, I tried to fix it by having B return a no_update if there is no new data. However, when the poll request B overlaps with a user request A, it seems that the no_update prevents the user request from properly updating.

My hack-ish fix: To prevent B from always calling callback A, even if there is no new data, I decoupled A from B. Instead of connecting the output of B to the input of callback A, a hidden button serves as an intermediary. Callback B triggers a client-side javascript function that checks the condition (is there new data?). If true, the hidden button is programmatically clicked, triggering callback A.

If this is useful to anyone, I would be glad to provide a code example.

Please provide a code example, it makes it much easier to understand your approach :slight_smile:

I tried to make a minimal (non-working) example. There are 3 layout components:
-a hidden div containing an interval component to do the polling
-a datatable that can be clicked by the user
-the graph displaying the data

There are 3 callbacks:
-updateOutputGraph displays a graph, when the user clicks the datatable, or when new data is received
-checkNewData polls data
-the client-side callback verifies

It works as follows. The server is polled, if new data is returned, is_new_data is set to True. This triggers the client-side callback. The condition (is_new_data == true) is checked, and if true, the dummy-button is clicked which triggers the updateOutputGraph callback. In cases where there is no new data, the update is not unnecessarily triggered.

While this approach is convoluted, it effectively realizes a conditional callback.

app.layout = html.Div(children =[
    # Hidden div that does the polling
    html.Div(children = [
            dcc.Interval(
                id='interval-component-10s',
                interval=10*1000, 
                n_intervals=0
            ),
            html.Div(id = "new_data", children =[]),        # This div stores the new data
            html.Div(id = "is_new_data", children = [False]),# This div keeps track of whether there is new data
            dbc.Button("dummy_button", id="dummy_button"),  # This button will be clicked by the client-side callback
            html.Div(id = "dummy2", children = [])          # Dummy div for the output, 
        ], style= {"display":'none'}
    ),

    # User can click this dataframe to display a certain row
    html.Div(
        children=[
            dash_table.DataTable(
                id= "data_selector",
                columns =[{"name": i, "id":i} for i in dataframe.columns],
                data = dataframe.to_dict('records'),
            )
        ]
    ),
    
    #Output is shown on the graph
     dcc.Graph(
        id='some_graph',
        figure=getSomeFigure()
    )
])



# This callback updates the displayed figure, either triggered by the user, or through new data by polling the server
@app.callback(
    [Output("some_graph",'figure')],
    [Input('data_selector', 'active_cell'),
     Input("dummy_button", 'n_clicks')],
    [State('data_selector','data'),
     State("new_data", 'children')]
)
def updateOutputGraph(active_cell,dummy, data_records, new_data):
    # Check which input has triggered the callback
    ctx = dash.callback_context
    if not ctx.triggered:
        button_id = 'No clicks yet'
    else:
        [button_id, propertyName] = ctx.triggered[0]['prop_id'].split('.');
    # If the user has triggered the callback by clicking the datatable, use the selected data
    if (button_id = 'data_selector'):
        # extract the correct data out of the dataframe
        extracted_data = extractTheData(data_records, active_cell)
    # The event was triggered by the dummy button
    else:
        extracted_data = new_data
    # possibly lenghty calculations using extracted_data
    return makeFigure(extracted_data)
               

# Callback that checks for new data every 10s, if there is new data, store it, and set is_new_data to true
@app.callback(
    [Output("new_data", 'children'),
     Output("is_new_data", 'children')],
    [Input('interval-component-10s', 'n_intervals')],
    [State("new_data", 'children')])
def updateNewData(n_intervals, previous_new_data):
    new_data = checkIfNewData() 
    if (new_data != None):
        return [new_data], [True]
    else:
        return previous_new_data,[False]


# The client-side callback checks whether there is new data, as stored in is_new_data
app.clientside_callback(
    """
    function(is_new_data) {
            // return value does not matter
            // probably need to take the first child of is_new_data
            if (is_new_data == true){
                dummy_button = document.getElementById("dummy_button")
                dummy_button.click()
                return [0]
            }
            return [0]            
        }
    """,
    Output("dummy2", 'children'),
    [Input("is_new_data", 'children')]
)