Determine what DataTable row was selected

Following the discovery of what (to me) looks like a bug (discrepancy between the actual selection of rows, and the value of derived_viewport_selected_row_ids - which looks like something I also reported a while back, I found one simple technique, which basically compares the selected_row_ids, derived_viewport_row_ids and (incorrect) derived_viewport_selected_row_ids to find the difference, and thereby determining the last ticked or unticked row in the table (without the need for a dcc.Store.

Example below.

I’m not sure however whether I can rely on this, given that this looks like a bug, and may get fixed one day.

from dash import Dash, dash_table, dcc, html
from dash.dependencies import Input, Output, State
import pandas as pd

df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/gapminder2007.csv')
df['id'] = df['country']

app = Dash(__name__)

app.layout = html.Div([
    html.P(id='placeholder'),

    dash_table.DataTable(
        id='datatable-interactivity',
        columns=[
            {"name": i, "id": i, "deletable": True, "selectable": True} for i in df.columns
        ],
        data=df.to_dict('records'),
        editable=True,
        filter_action="native",
        sort_action="native",
        sort_mode="multi",
        column_selectable="single",
        row_selectable="multi",
        row_deletable=True,
        selected_columns=[],
        selected_rows=[],
        page_action="native",
        page_current= 0,
        page_size= 10,
    ),
    html.Div(id='datatable-interactivity-container')
])

@app.callback(
    Output('datatable-interactivity', 'style_data_conditional'),
    Input('datatable-interactivity', 'selected_columns')
)
def update_styles(selected_columns):
    return [{
        'if': { 'column_id': i },
        'background_color': '#D2F3FF'
    } for i in selected_columns]

@app.callback(
    Output('datatable-interactivity-container', "children"),
    Input('datatable-interactivity', "derived_virtual_data"),
    Input('datatable-interactivity', "derived_virtual_selected_rows"))
def update_graphs(rows, derived_virtual_selected_rows):
    # When the table is first rendered, `derived_virtual_data` and
    # `derived_virtual_selected_rows` will be `None`. This is due to an
    # idiosyncrasy in Dash (unsupplied properties are always None and Dash
    # calls the dependent callbacks when the component is first rendered).
    # So, if `rows` is `None`, then the component was just rendered
    # and its value will be the same as the component's dataframe.
    # Instead of setting `None` in here, you could also set
    # `derived_virtual_data=df.to_rows('dict')` when you initialize
    # the component.
    if derived_virtual_selected_rows is None:
        derived_virtual_selected_rows = []

    dff = df if rows is None else pd.DataFrame(rows)

    colors = ['#7FDBFF' if i in derived_virtual_selected_rows else '#0074D9'
              for i in range(len(dff))]

    return [
        dcc.Graph(
            id=column,
            figure={
                "data": [
                    {
                        "x": dff["country"],
                        "y": dff[column],
                        "type": "bar",
                        "marker": {"color": colors},
                    }
                ],
                "layout": {
                    "xaxis": {"automargin": True},
                    "yaxis": {
                        "automargin": True,
                        "title": {"text": column}
                    },
                    "height": 250,
                    "margin": {"t": 10, "l": 10, "r": 10},
                },
            },
        )
        # check if column exists - user may have deleted it
        # If `column.deletable=False`, then you don't
        # need to do this check.
        for column in ["pop", "lifeExp", "gdpPercap"] if column in dff
    ]

@app.callback(
    [Output("placeholder", "children")],
    [Input("datatable-interactivity", "selected_row_ids")],
    [
        State("datatable-interactivity", "derived_viewport_row_ids"),
        State("datatable-interactivity", "derived_viewport_selected_row_ids"),
        State("datatable-interactivity", "derived_virtual_row_ids"),
        State("datatable-interactivity", "derived_virtual_selected_row_ids"),
        State("datatable-interactivity", "data")
    ])
def see_row_ids(selected_row_ids, derived_viewport_row_ids, derived_viewport_selected_row_ids, derived_virtual_row_ids,
                derived_virtual_selected_row_ids, data):
    out = []
    if selected_row_ids:
        out.append(html.P("selected_row_ids ({}): {}\n".format(len(selected_row_ids), selected_row_ids)))
    if derived_viewport_row_ids:
        out.append(html.P("derived_viewport_row_ids ({}): {}".format(len(derived_viewport_row_ids), derived_viewport_row_ids)))
    if not derived_viewport_selected_row_ids:
        derived_viewport_selected_row_ids = []
    out.append(html.P("derived_viewport_selected_row_ids ({}): {}".format(len(derived_viewport_selected_row_ids), derived_viewport_selected_row_ids)))
    if derived_virtual_row_ids:
        out.append(html.P("derived_virtual_row_ids ({}): {}".format(len(derived_virtual_row_ids), derived_virtual_row_ids)))
    if not derived_virtual_selected_row_ids:
        derived_virtual_selected_row_ids = []
    out.append(html.P("derived_virtual_selected_row_ids ({}): {}".format(len(derived_virtual_selected_row_ids), derived_virtual_selected_row_ids)))

    all_selected = selected_row_ids
    if not all_selected:
        all_selected = []
    if not derived_virtual_row_ids:
        derived_virtual_row_ids = []
    selected_in_view = [x for x in derived_virtual_row_ids if x in all_selected]
    previously_selected_in_view = derived_viewport_selected_row_ids
    last_selection = [x for x in selected_in_view if x not in previously_selected_in_view]
    last_deselection = [x for x in previously_selected_in_view if x not in selected_in_view]
    if last_selection:
        out.append(html.P(f"last selected {last_selection}"))
    if last_deselection:
        out.append(html.P(f"last deselected {last_deselection}"))

    # difference = [x for x in ]

    return [out]


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