Bug with "derived viewport selected row ids"?

Unless there is something I don’t understand about what derived_viewport selected_row_ids is, I think I may have found a bug with the DataTable.

It seems to contain the list of row ids in the viewport before the user made a selection (that triggered the callback).

Here are 2 screenshots showing this:

user clicks on second row to select it:

user clicks on second row again to unselect it:

It always seem to lag one step behind the user selection.

Here is the callback that displays the plain text above the table:

@app.callback(
    [Output("placeholder", "children")],
    [Input("table_encodings", "selected_row_ids")],
    [
        State("table_encodings", "derived_viewport_row_ids"),
        State("table_encodings", "derived_viewport_selected_row_ids"),
        State("table_encodings", "derived_virtual_row_ids"),
        State("table_encodings", "data")
    ])
def see_row_ids(selected_row_ids, derived_viewport_row_ids, derived_viewport_selected_row_ids, derived_virtual_row_ids, data):
    out = []
    out.append(html.P("selected_row_ids ({}): {}\n".format(len(selected_row_ids), selected_row_ids)))
    out.append(html.P("derived_viewport_selected_row_ids ({}): {}<br/>".format(len(derived_viewport_selected_row_ids), derived_viewport_selected_row_ids)))

    return [out]

I see the same behaviour with derived_virtual_selected_row_ids

I’ve reproduced this with a simple example based on the current documentation on filtering and sorting:

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}"))

    return [out]


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

Hi all,

I know this is a quite old thread. However, this bug is still present in the recent version of dash (Version: 2.3.1). Any updates on this bug? Or work arounds for this?

For context this feature is quite handy in project where you update the table based upon an input of the user. For example a dropdown menu (with multiple selections) that selects ‘Countries’ and then shows records of that country in a datatable. Next, the selected rows of the datatable are then visualized in a graph.

If you would work with ‘selected_rows’ attribute you can have bugs that not visible rows are selected and displayed in your graph. Thus your user have to unselect each row before remove the records from the table.

See the documentation on Sorting, Filtering, Selecting, and Paging Natively

Any advice is welcome!

hi @F2P
Thank you for following up on this topic. Are you able to share a minimum working example? Do you have your own code that generates this unwanted behavior?

Hi Adam,

The example provided in the initial post is a minimal example showing this bug. The example is taken from the documentation with printouts of the following attributes: selected_row_ids, derived_viewport_row_ids, derived_viewport_selected_row_ids, derived_virtual_selected_row_ids

Just by checking/unchecking a few rows you can see that some attributes (derived_viewport_selected_row_ids, derived_virtual_selected_row_ids) are lagging (~show the previous state before the click).

If you need additional clarification please let me know.

hi @F2P
The reason I was asking for your code is that I thought you had some code you wrote already, which I would have tried to help you with.

I tried to simplify the example provided by @Wabiloo above to see what’s happening only with the derived_viewport_selected_row_ids.
It appears that if the callback has derived_viewport_selected_row_ids as an Input argument, it doesn’t lag and it works. See the print statement on row 108 in the code below:

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"),
    Input("datatable-interactivity", "derived_viewport_selected_row_ids"),
    [
        State("datatable-interactivity", "derived_viewport_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(derived_viewport_selected_row_ids, derived_viewport_row_ids, derived_virtual_row_ids,
                derived_virtual_selected_row_ids, data):
    print(derived_viewport_selected_row_ids)
    # 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}"))

    return [""]


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

However, if you add the first Input selected_row_ids that I hashtagged out on row 98 (with the respective argument in the callback function), and make the derived_viewport_selected_row_ids into a State argument, the print statement lags behind and only the previous selected row ID gets printed.

1 Like

Hi Adam!

Sorry for not provide an example, but indeed I had a similar callback where my Input was selected_row_ids and State was derived_virtual_selected_row_ids.

Nice found! @Wabiloo and me made the mistake of thinking that the State objects where updated upon user selection of the rows. I was wondering if an additional explanation (avoid using State) is needed to be added to the documentation of these attributes (see previous post for the link)?

Thanks for the help though, was breaking my head over this bug for the last hour! :grinning:

2 Likes