Dash: How to select data points either in the table which updates the plot, or on the graph which updates the selection on the table?

The problem

It would be great to make both callbacks below work side by side. I can make either the first or the second one work by commenting out the other, but I can’t figure out how to make them work together. The problem is that it’s not allowed/possible to use “Duplicate callback outputs” (I assume because this would lead to an infinite loop).

Requirements

  • pandas
  • dash
  • plotly
  • dash_bootstrap_components

Code

import pandas as pd
import dash
from dash.dependencies import Input, Output
import dash_table
import dash_core_components as dcc
import dash_html_components as html
import dash_bootstrap_components as dbc
import plotly.graph_objects as go

df = pd.DataFrame.from_dict(
    {'term': {0: 'GOCC:0043229', 1: 'GOCC:0098588', 2: 'GOCC:0005730', 3: 'GO:0005730', 4: 'GO:0005783', 5: 'GO:0031410', 6: 'KW-0732', 7: 'KW-0156', 8: 'KW-0010'},
    'description': {0: 'Intracellular organelle', 1: 'Bounding membrane of organelle', 2: 'Nucleolus', 3: 'nucleolus', 4: 'endoplasmic reticulum', 5: 'cytoplasmic vesicle', 6: 'Signal', 7: 'Chromatin regulator', 8: 'Activator'},
     'FG_count': {0: 370, 1: 92, 2: 126, 3: 500, 4: 63, 5: 23, 6: 9, 7: 410, 8: 31},
     'logFDR': {0: 2.6, 1: 4, 2: 5, 3: 2, 4: 7, 5: 8, 6: 5, 7: 1, 8: 6},
     'effectSize': {0: 0.053, 1: -0.049, 2: 0.046, 3: 0.025, 4: -0.040, 5: -0.027, 6: -0.024, 7: 0.025, 8: 0.047},
     'category': {0: 'TM', 1: 'TM', 2: 'TM', 3: 'GOCC', 4: 'GOCC', 5: 'GOCC', 6: 'UPK', 7: 'UPK', 8: 'UPK'}})

style_data_conditional_basic = [{"if": {"state": "selected"}, "backgroundColor": "gold", "border": "inherit !important", "text_align": "inherit !important", }] + [{"if": {"state": "active"}, "backgroundColor": "inherit !important", "border": "inherit !important", "text_align": "inherit !important", }]

app = dash.Dash(__name__, prevent_initial_callbacks=True, external_stylesheets=[dbc.themes.BOOTSTRAP])
marker_size_min_ = 4
sizeref = 0.05

data_table_ex = dash_table.DataTable(
                id='main_datatable',
                columns=[{"name": colName, "id": colName} for colName in df.columns],
                data=df.to_dict('records'),
                sort_action="native",
                row_selectable="multi",
                selected_columns=[],
                selected_rows=[],
                style_as_list_view=True,
                style_data={'if': {'row_index': 'odd'}, 'backgroundColor': "#F5F5F5", },
                style_data_conditional=[],
                style_cell={'minWidth': "10px", "width": "50px", "maxWidth": "80px", "fontSize": "12px", "font-family": "sans-serif", "text_align": "center", "border": "1px",},)

def create_scatter_plot_graph(df):
    fig = go.Figure()
    for category_name, group in df.groupby("category"):
        fig.add_trace(go.Scatter(name=category_name, x=group["logFDR"].tolist(), y=group["effectSize"].tolist(), ids=group["term"].tolist(), legendgroup=category_name, mode="markers", marker_symbol="circle", marker_size=group["FG_count"], marker_sizemin=marker_size_min_, marker_sizemode="area", marker_sizeref=sizeref, customdata=[list(ele) for ele in zip(group["term"], group["description"], group["FG_count"])], hovertemplate="<b>%{customdata[0]}</b><br>%{customdata[1]}<br>Size: %{customdata[2]}<extra></extra>", ))
    fig.update_layout(xaxis={'title': 'log(FDR)'}, yaxis={'title': 'effect size'})
    scatter_plot_graph = dcc.Graph(id='scatter_plot', figure=fig)
    return scatter_plot_graph

scatter_plot_graph = create_scatter_plot_graph(df)

app.layout = html.Div(id='general_div', className="container-fluid",
    children=[
        dbc.Row([
            dbc.Col(
                html.Div(id="scatter_container", children=[scatter_plot_graph]), xs={"size": 12}, sm={"size": 12}, md={"size": 10}, lg={"size": 8}, ),
        ], justify="center", ),
        html.Br(),
        dbc.Row([
            dbc.Col(
                html.Div(data_table_ex), xs={"size": 12}, sm={"size": 12}, md={"size": 10}, lg={"size": 10},),
        ], justify="center",),
        html.Br(),
        ])

############ Select either "Option 1" or "Option 2" and comment out the other ############
### Option 1 START
@app.callback([Output(component_id="main_datatable", component_property="style_data_conditional"),
               Output(component_id="main_datatable", component_property="selected_rows")],
              [Input(component_id="scatter_plot", component_property="selectedData")])
def on_selectInScatter_highlight_and_select_dataTableRows(selectedData):
    selected_term_list, selected_rows = [], []
    if selectedData is not None:
        for point in selectedData["points"]:
            selected_term_list.append(point["customdata"][0])
        style_data_conditional_extension = [{'if': {'filter_query': '{term}='+"{}".format(term)}, 'backgroundColor': 'gold'} for term in selected_term_list]
        selected_rows = df[df["term"].isin(selected_term_list)].index.tolist()
        return style_data_conditional_extension + style_data_conditional_basic, selected_rows
    return style_data_conditional_basic, selected_rows
### Option 1 STOP
###############################
### Option 2 START
# @app.callback([Output(component_id="main_datatable", component_property="style_data_conditional"),
#                Output(component_id="scatter_container", component_property="children")],
#               [Input(component_id="main_datatable", component_property="selected_rows"),
#                Input(component_id="main_datatable", component_property="derived_virtual_data"),
#                Input(component_id="main_datatable", component_property='derived_virtual_selected_rows')])
# def on_selectInDataTable_highlight_dataTableRows_and_pointsInScatterPlot(selected_rows, derived_virtual_data, derived_virtual_selected_rows):
#     dff = df if len(derived_virtual_data) == 0 else pd.DataFrame(derived_virtual_data)
#     dff["marker_line_width"] = 1
#     dff["marker_line_color"] = "white"
#     dff.loc[derived_virtual_selected_rows, "marker_line_width"] = 4
#     dff.loc[derived_virtual_selected_rows, "marker_line_color"] = "black"
#     fig = go.Figure()
#     for category_name, group in dff.groupby("category"):
#         fig.add_trace(go.Scatter(name=category_name, x=group["logFDR"].tolist(), y=group["effectSize"].tolist(), ids=group["term"].tolist(), legendgroup=category_name, mode="markers", marker_symbol="circle", marker_size=group["FG_count"], marker_sizemin=marker_size_min_, marker_sizemode="area", marker_sizeref=sizeref, marker_line_width=group["marker_line_width"], marker_line_color=group["marker_line_color"], customdata=[list(ele) for ele in zip(group["term"], group["description"], group["FG_count"])], hovertemplate="<b>%{customdata[0]}</b><br>%{customdata[1]}<br>Size: %{customdata[2]}<extra></extra>", ))
#     fig.update_layout(xaxis={'title': 'log(FDR)'}, yaxis={'title': 'effect size'})
#     scatter_plot_fig = dcc.Graph(id='scatter_plot', figure=fig)
# 
#     if selected_rows is not None:
#         selected_term_list = dff.loc[selected_rows, "term"].tolist()
#         style_data_conditional_extension = [{'if': {'filter_query': '{term}=' + "{}".format(term)}, 'backgroundColor': 'gold'} for term in selected_term_list]
#         return style_data_conditional_extension + style_data_conditional_basic, scatter_plot_fig
#     else:
#         return style_data_conditional_basic, scatter_plot_fig
### Option 2 STOP
####################################
############ Select one of the two options above and comment out the other ############

if __name__ == '__main__':
    app.run_server(debug=True, host="127.0.0.1", port=5922)
1 Like

Really nice solution. I am wondering if it is possible to join both approaches in the same application.
Selecting the points, update the checkbox and then unselect some checkbox updating the row color.

If you combine the two callbacks into one, it should be possible.

1 Like

Hi Emil, thanks for the answer! I tried to, but I got this “Duplicate callback outputs”, on which I don’t know how to solve. Do you have any advice?

That’s because you target an element as output in multiple callbacks. You should avoid that.