✊🏿 Black Lives Matter. Please consider donating to Black Girls Code today.
🐇 Announcing Dash VTK for 3d simulation graphics. Check out the March webinar.

Data sharing between callbacks / getting the trigger right

Currently I’m tying to implement workaround #3 from here: DataTable with filtering as well as components within cells

So basically I have a datatable and want to display additional information for a specific row when clicking on it. I have a first somewhat working version basically looking like that

infobox_clicks = 0

...

html.Div(id='infobox', style={'display': 'none'})

...

@app.callback(
    Output('infobox', 'style'),
    [Input('infobox', 'children'),
     Input('infobox', 'n_clicks')])
def update_infobox_style(children, n_clicks):
    global infobox_clicks
    style = {'height': '300px', 'background-color': '#fff', 'position': 'fixed', 'margin': 'auto', 'top': '0', 'bottom': '0', 'left': '0', 'right': '0', 'border': '1px solid'}
    if children and n_clicks == infobox_clicks or not n_clicks:
        style['display'] = 'initial'
        style['opacity'] = .85
        return style
    elif n_clicks > infobox_clicks:
        style['display'] = 'none'
        infobox_clicks += 1
        return style


@app.callback(
    Output('infobox', 'children'),
    [Input('scenarios', 'active_cell'),
     Input('scenarios', 'data')])
def get_selected_scenario(active_cell, data):
    if active_cell:
        return scenario_info_html(data[active_cell[0]], comm)

with scenario_info_html either returning some HTML elements or False. So the information to display gets stored int he infobox, but that one is just displayed when clicked on it and hidden again when clicked again.

But the problem is this stupid click counter. I theory everything would be fine by just using a n_clicks event on update_infobox_style, however that doesn’t work out since Dash doesn’t tell me which one of the inputs triggered the callback.

And of course this counter breaks once there are several people using it or the first one is just reloading the page. However packing the counter in an additional hidden div instead of a global variable is no solution either since update_infobox_style needs to write to it. But unfortunately callbacks are not allowed to have more than one output.

And even the messy option of coding the counter information into the infobox style doesn’t work out since a callback can’t have an output also as input, e.g. the following fails:

@app.callback(
    Output('infobox', 'style'),
    [Input('infobox', 'children'),
     Input('infobox', 'n_clicks'),
     Input('infobox', 'style')])
def update_infobox_style(children, n_clicks, style):
    ...

Is there some clean solution to that?

I found a solution myself using a combination of n_clicks_timestamp and State. Since n_clicks_timestamp isn’t available for all elements (e.g. datatables) I wrapped all elements in divs. The new update_infobox_style callback now looks as follows

@app.callback(
    Output('infobox', 'style'),
    [Input('infobox', 'n_clicks_timestamp'),
     Input('scenario_div', 'n_clicks_timestamp')],
    [State('scenarios', 'active_cell'),
     State('scenarios', 'data')])
def update_infobox_style(infobox_click_time, scenario_click_time, active_cell, data):
    style = {'height': '300px', 'background-color': '#fff', 'position': 'fixed', 'margin': 'auto', 'top': '0', 'bottom': '0', 'left': '0', 'right': '0', 'border': '1px solid'}
    children = None

    if active_cell:
        children = scenario_info_html(data[active_cell[0]], comm)

    if children and int(scenario_click_time) > int(infobox_click_time):
        style['display'] = 'initial'
        style['opacity'] = .85
    else:
        style['display'] = 'none'

    return style

Unfortunately I have to generate the children a second time, this is to avoid race conditions between this callback and get_selected_scenario