How to manage row selection in dash datatable when filtering is done through components?

Hello,
I have an app with filters components, a chart and a datatable. The datatable has multi selection and native filtering.
I cannot get the callback structure right around persistence of row selection. I have looked at the forum + chatGPT, no luck and I need help!

The behaviour I am expecting is :

  • when the data is filtered with components this change the color in the chart AND the visible rows in the datatable
  • when the datatable is filtered this only change the rows visible in the table (if it changes the chart as well this is fine)
  • the user can then select rows in the table and the selected rows are highlighted in the chart
    basically you use a combination of table and components filtering to display the result and allow to select certain rows by playing with filters.

Everything works fine but the issue is that if I select at some point for example the second row, and change the component filter, the selected row remain row two but it is not the item I have initially selected…

I hope this is clear.
Any help would be very much appreciated.

At the moment I have only one callback that update the chart and I use selected_rows.
I don’t have store components.

I could not managed to create a working minimal example of app doing the above with chatGPT fyi.
Many thanks in advance!

Hello!

I don’t understand why you can’t provide a working minimal example if you say that “everything is working fine”. It’s hard to help you without, to be honest. :slight_smile:
What’s your callback ? What is the pseudo code if you can share a code ?

Here is a minimal example, without chart, showcasing the issue I am facing.
I would like row selection to be maintained after filters are changed by user.
Ideally selected rows should remain visible (even if not meeting new filters) and at the top of the table.
Currently this example messes up the row selection when filtering changes.

import dash
from dash import dcc, html, Input, Output, State, dash_table, callback_context, no_update
import pandas as pd

print("Starting...")

df = pd.DataFrame({
    "id": range(1, 11),
    "category": ["A", "B"] * 5,
    "value": [10, 20, 15, 25, 30, 45, 50, 35, 40, 55],
})

app = dash.Dash(__name__)

app.layout = html.Div([
    html.Div([
        dcc.Dropdown(
            id="category-filter",
            options=[{"label": cat, "value": cat} for cat in df["category"].unique()],
            multi=True,
            placeholder="Filter by category",
        ),
    ]),

    dash_table.DataTable(
        id="data-table",
        columns=[{"name": col, "id": col} for col in df.columns],
        data=df.to_dict("records"),
        row_selectable="multi",
        selected_rows=[],
        filter_action="native",
        page_size=25,
    ),
])


n = 0

@app.callback(
    Output("data-table", "data"),
    Input("category-filter", "value"),
    Input("data-table", "selected_rows"),
    State("data-table", "derived_virtual_data"),
)
def update_table(filter_values, selected_rows, data):
    """
    Update table based on filter component, datatable native filters, maintain row selection
    Filters:
        - limit rows that are shown in table (ideally previoulsy selected rows remain and appear at the top)
    datatable filters:
        - limit rows that are shown in table (ideally any previoulsy selected rows remain and appear at the top)
    row selection:
        - will change a chart component
    """
    global n

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

    print(f"{n}_Callback trigger with ctx prop: {prop_id}")
    
    if selected_rows:
        print(f"{n}_ Number of rows selected: {len(selected_rows)}")
        selected = df.iloc[selected_rows]
        print(f"{n}_ Selected: {selected}")
    if data:
        print(f"{n}_ Number of rows shown: {len(data)}")

    dff = df
    n += 1

    if triggered_id == "category-filter":
        print(f"{n}_FILTERS")
        # Apply filtering
        if filter_values:
            dff = df[df["category"].isin(filter_values)]
            print(f"{n}_dff updated, new len: {len(dff)}")
        else:
            dff = df
    elif prop_id == "data-table.selected_rows":
        print(f"{n}_ROW SELECTION")
    elif prop_id == "data-table.derived_virtual_data":
        print(f"{n}_DATATABLE FILTER {prop_id}")
    else:
        print(f"{n}_Other event... {prop_id}")


    return dff.to_dict("records")


if __name__ == '__main__':
    app.run_server(debug=True)

Many thanks in advance for your help!

Hello!
Thank you for the code.

The problem is : you need to update selected_rows when updating the data table, because the ids you had before filtering will not correspond to the ids you have after filtering.

With your example

   id category  value
0   1        A     10
1   2        B     20
2   3        A     15
3   4        B     25
4   5        A     30
5   6        B     45
6   7        A     50
7   8        B     35
8   9        A     40
9  10        B     55

If you select on rows with id=[2, 4, 6] then you’ll have selected_rows=[1, 2, 3]
Then, you use the dropdown filter to select only category B.
The table gets updated to :

   id category  value
1   2        B     20
3   4        B     25
5   6        B     45
7   8        B     35
9  10        B     55

So in this case, you need to update selected_rows to [0, 1, 2], because ids 1, 2 and 3 are respectively in position 0, 1, and 2 of the dataframe. Having selected_rows=[1, 2, 3] won’t work anymore.

But you’ll soon struggle to keep track of the correct ids / indexes to use. To avoid this, you can set a specific index to your dataframe and use selected_row_ids in place of selected_rows.
Learn more about this here: Sorting, Filtering, Selecting, and Paging Natively | Dash for Python Documentation | Plotly (row IDs section).

Here is a code that worked for this example.

import dash
from dash import dcc, html, Input, Output, State, dash_table, callback_context, no_update
import pandas as pd

print("Starting...")

df = pd.DataFrame({
    "id": range(1, 11),
    "category": ["A", "B"] * 5,
    "value": [10, 20, 15, 25, 30, 45, 50, 35, 40, 55],
})

print(df)

df.set_index("id", inplace=True, drop=False)

app = dash.Dash(__name__)

app.layout = html.Div([
    html.Div([
        dcc.Dropdown(
            id="category-filter",
            options=[{"label": cat, "value": cat} for cat in df["category"].unique()],
            multi=True,
            placeholder="Filter by category",
        ),
    ]),

    dash_table.DataTable(
        id="data-table",
        columns=[{"name": col, "id": col} for col in df.columns],
        data=df.to_dict("records"),
        row_selectable="multi",
        selected_rows=[],
        filter_action="native",
        page_size=25,
    ),
])


n = 0

@app.callback(
    Output("data-table", "data"),
    Output("data-table", "selected_rows"),
    Input("category-filter", "value"),
    Input("data-table", "selected_row_ids"),
    State("data-table", "derived_virtual_data"),
)
def update_table(filter_values, selected_row_ids, data):
    """
    Update table based on filter component, datatable native filters, maintain row selection
    Filters:
        - limit rows that are shown in table (ideally previoulsy selected rows remain and appear at the top)
    datatable filters:
        - limit rows that are shown in table (ideally any previoulsy selected rows remain and appear at the top)
    row selection:
        - will change a chart component
    """
    global n

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

    print(f"\n{n}_Callback trigger with ctx prop: {prop_id}")

    if selected_row_ids:
        print(f"{n}_ Number of rows selected: {len(selected_row_ids)}")
        selected = df.loc[selected_row_ids]  # Changed iloc to loc here
        print(f"{n}_ Selected: \n{selected}")
    if data:
        print(f"{n}_ Number of rows shown: {len(data)}")

    dff = df
    n += 1

    if triggered_id == "category-filter":
        print(f"{n}_FILTERS")
        # Apply filtering
        if filter_values:
            dff = df[df["category"].isin(filter_values)]
            print(f"{n}_dff updated, new len: {len(dff)}")
        else:
            dff = df
    elif prop_id == "data-table.selected_rows":
        print(f"{n}_ROW SELECTION")
    elif prop_id == "data-table.derived_virtual_data":
        print(f"{n}_DATATABLE FILTER {prop_id}")
    else:
        print(f"{n}_Other event... {prop_id}")

    print("dff")
    print(dff)

    print("selected_row_ids", selected_row_ids)

    selected_row_ids = selected_row_ids or []
    selected_rows = [i for i, index in enumerate(dff.index) if index in selected_row_ids]
    print("selected_rows will be ", selected_row_ids)

    return dff.to_dict("records"), selected_rows


if __name__ == '__main__':
    app.run_server(debug=True)

I did not test it further (interaction with the native filter component, etc.)
But at least you have new insights (I hope).

Hope it helps :slight_smile:

1 Like

Thank you so much for your help, this is exactly what I needed! I didn’t know about selected_row_ids! It took me a bit of time to implement in my project which is more convoluted but it works! I need to implement the same for active_cell!
Thanks again!

1 Like

@ysiegel you’re welcome!

Hello,
I have realised that the code that you provided does not work as expected in all circumstances, and I don’t know how to solve this…
If you try to:

  • select id 5
  • filter on B
  • select id 4

=> the filtering is lost and id 5 is not anymore selected

Any idea on how to address that?

Also do you see a way for the selected rows to be always visible and at the top? even when sorting or filtering is applied?

Many thanks!