AG Grid Row Selection Help

I’ve built a Dash web app with an AG Grid table. The app dynamically loads data based on user inputs. In addition, when the user selects a row in the table, this also triggers updates. What I have been struggling with is how to have the table show the first row as the selected row whenever data is loaded into the table. The problem is that when the selected row state is changed, this triggers the callback a second time, and this second callback somehow unselects the row.

I’ve modified a Dash AG Grid example to illustrate my problem. How can this code be modified so that the first row is automatically selected when data is loaded into the table (by clicking on the button)?

import dash_ag_grid as dag
from dash import Dash, html, Input, Output, callback, no_update, ctx
import pandas as pd

app = Dash(__name__)

df = pd.read_csv(
    "https://raw.githubusercontent.com/plotly/datasets/master/ag-grid/olympic-winners.csv"
)

columnDefs = [{"field": i} for i in ["country", "year", "athlete", "age", "sport", "total"]]

app.layout = html.Div(
    [
        html.Button("Load Data", id="btn-load-data"),
        dag.AgGrid(
            id="row-selection-selected-rows",
            columnDefs=columnDefs,
            columnSize="autoSize",
            dashGridOptions={"rowSelection": "single", "animateRows": False},
        ),
        html.Pre(id="pre-row-selection-selected-rows", style={'text-wrap': 'wrap'})
    ],
)


@callback(
    Output("row-selection-selected-rows", "rowData"),
    Output("row-selection-selected-rows", "selectedRows"),
    Output("pre-row-selection-selected-rows", "children"),
    Input("btn-load-data", "n_clicks"),
    Input("row-selection-selected-rows", "selectedRows")
)
def output_selected_rows(_, selected_rows):
    print(ctx.triggered_id)
    if ctx.triggered_id == "btn-load-data":
        return df.to_dict("records"), df.head(1).to_dict("records"), ""
    elif ctx.triggered_id == "row-selection-selected-rows":
        selected_list = [f"{s['athlete']} ({s['year']})" for s in selected_rows]
        msg = f"You selected the athlete{'s' if len(selected_rows) > 1 else ''}:\n{', '.join(selected_list)}" if selected_rows else "No selections"
        return no_update, no_update, msg
    return no_update, no_update, ""

if __name__ == "__main__":
    app.run(debug=True)

@jinnyzor I see you’ve been helping with people’s struggles with AG Grid. Could you help with this?

Hello @adiadidas15,

What version of the grid are you using?

@jinnyzor my dash-ag-grid Python package is version 31.0.1.

The selectedRows acts a little different in v31:

import dash_ag_grid as dag
from dash import Dash, html, Input, Output, callback, no_update, ctx
import pandas as pd

app = Dash(__name__)

df = pd.read_csv(
    "https://raw.githubusercontent.com/plotly/datasets/master/ag-grid/olympic-winners.csv"
)

columnDefs = [{"field": i} for i in ["country", "year", "athlete", "age", "sport", "total"]]

app.layout = html.Div(
    [
        html.Button("Load Data", id="btn-load-data"),
        dag.AgGrid(
            id="row-selection-selected-rows",
            columnDefs=columnDefs,
            columnSize="autoSize",
            dashGridOptions={"rowSelection": "single", "animateRows": False},
        ),
        html.Pre(id="pre-row-selection-selected-rows", style={'text-wrap': 'wrap'})
    ],
)


@callback(
    Output("row-selection-selected-rows", "rowTransaction"),
    Output("row-selection-selected-rows", "selectedRows"),
    Output("pre-row-selection-selected-rows", "children"),
    Input("btn-load-data", "n_clicks"),
    Input("row-selection-selected-rows", "selectedRows")
)
def output_selected_rows(_, selected_rows):
    print(ctx.triggered_id)
    if ctx.triggered_id == "btn-load-data":
        return {'add': df.to_dict("records"), 'async': False}, df.head(1).to_dict("records"), ""
    elif ctx.triggered_id == "row-selection-selected-rows":
        selected_list = [f"{s['athlete']} ({s['year']})" for s in selected_rows]
        msg = f"You selected the athlete{'s' if len(selected_rows) > 1 else ''}:\n{', '.join(selected_list)}" if selected_rows else "No selections"
        return no_update, no_update, msg
    return no_update, no_update, ""

if __name__ == "__main__":
    app.run(debug=True)

rowData is performed asynchronously now, so you need to use rowTransaction instead.

If you need to clear the data, then you can use the rowData to set [] and then perform the transaction above.

Thanks @jinnyzor! That’s exactly what I needed!

@jinnyzor I had flagged your response as the solution but after further testing I’ve realized it doesn’t quite work. Your response works fine as long as the user only clicks on the Load Data button once. But on subsequent clicks the first row will not be highlighted. Presumably this is happening because duplicate data is being added to the table.

Since I don’t want duplicate data, I’ve added a chained callback (based on what you said above I assume I can’t remove data and add data with one transaction?). The first callback clears the table of all data. Then second callback (output_selected_rows) does all the updating. For reasons I don’t understand, the second callback now becomes an infinite loop, calling itself endlessly.

import dash_ag_grid as dag
from dash import Dash, html, Input, Output, callback, no_update, ctx
import pandas as pd

app = Dash(__name__)

df = pd.read_csv(
    "https://raw.githubusercontent.com/plotly/datasets/master/ag-grid/olympic-winners.csv"
)

columnDefs = [{"field": i} for i in ["country", "year", "athlete", "age", "sport", "total"]]

app.layout = html.Div(
    [
        html.Button("Load Data", id="btn-load-data"),
        dag.AgGrid(
            id="row-selection-selected-rows",
            columnDefs=columnDefs,
            columnSize="autoSize",
            dashGridOptions={"rowSelection": "single", "animateRows": False},
        ),
        html.Pre(id="pre-row-selection-selected-rows", style={'text-wrap': 'wrap'})
    ],
)


@callback(
    Output("row-selection-selected-rows", "rowData"),
    Input("btn-load-data", "n_clicks"),
    prevent_initial_call=True,
)
def clear_table(_):
    return []


@callback(
    Output("row-selection-selected-rows", "rowTransaction"),
    Output("row-selection-selected-rows", "selectedRows"),
    Output("pre-row-selection-selected-rows", "children"),
    Input("row-selection-selected-rows", "rowData"),
    Input("row-selection-selected-rows", "selectedRows"),
    prevent_initial_call=True,
)
def output_selected_rows(_, selected_rows):
    print(ctx.triggered_prop_ids)
    if "row-selection-selected-rows.rowData" in ctx.triggered_prop_ids.keys():
        return {'add': df.to_dict("records"), 'async': False}, df.head(1).to_dict("records"), ""
    else:
        selected_list = [f"{s['athlete']} ({s['year']})" for s in selected_rows]
        msg = f"You selected the athlete{'s' if len(selected_rows) > 1 else ''}:\n{', '.join(selected_list)}" if selected_rows else "No selections"
        return no_update, no_update, msg

if __name__ == "__main__":
    app.run(debug=True)

I can break this infinite loop by using a Store to trigger the second callback instead of rowData. This is what I’ve coded below. This almost achieves the desired behavior. On the first click of the Load Data button the data loads but output_selected_rows gets triggered a second time and the first row isn’t shown as selected (because the table has duplicate data?). On all subsequent clicks of the Load Data button the app behaves as I expect: each of the callbacks are only triggered once. Any ideas how to fix this?

import dash_ag_grid as dag
from dash import Dash, html, Input, Output, State, callback, no_update, ctx, dcc
import pandas as pd

app = Dash(__name__)

df = pd.read_csv(
    "https://raw.githubusercontent.com/plotly/datasets/master/ag-grid/olympic-winners.csv"
)

columnDefs = [{"field": i} for i in ["country", "year", "athlete", "age", "sport", "total"]]

app.layout = html.Div(
    [
        html.Button("Load Data", id="btn-load-data"),
        dag.AgGrid(
            id="row-selection-selected-rows",
            columnDefs=columnDefs,
            columnSize="autoSize",
            dashGridOptions={"rowSelection": "single", "animateRows": False},
        ),
        html.Pre(id="pre-row-selection-selected-rows", style={'text-wrap': 'wrap'}),
        dcc.Store(id="table-clear-flag")
    ],
)


@callback(
    Output("row-selection-selected-rows", "rowData"),
    Output("table-clear-flag", "data"),
    Input("btn-load-data", "n_clicks"),
    State("table-clear-flag", "data"),
    prevent_initial_call=True,
)
def clear_table(_, flag):
    return [], not flag


@callback(
    Output("row-selection-selected-rows", "rowTransaction"),
    Output("row-selection-selected-rows", "selectedRows"),
    Output("pre-row-selection-selected-rows", "children"),
    Input("table-clear-flag", "data"),
    Input("row-selection-selected-rows", "selectedRows"),
    prevent_initial_call=True,
)
def output_selected_rows(_, selected_rows):
    print(ctx.triggered_id)
    if ctx.triggered_id == "table-clear-flag":
        return {'add': df.to_dict("records"), 'async': False}, df.head(1).to_dict("records"), ""
    else:
        selected_list = [f"{s['athlete']} ({s['year']})" for s in selected_rows]
        msg = f"You selected the athlete{'s' if len(selected_rows) > 1 else ''}:\n{', '.join(selected_list)}" if selected_rows else "No selections"
        return no_update, no_update, msg

if __name__ == "__main__":
    app.run(debug=True)

Try this:

import dash_ag_grid as dag
from dash import Dash, html, Input, Output, callback, no_update, ctx
import pandas as pd

app = Dash(__name__)

df = pd.read_csv(
    "https://raw.githubusercontent.com/plotly/datasets/master/ag-grid/olympic-winners.csv"
)

columnDefs = [{"field": i} for i in ["country", "year", "athlete", "age", "sport", "total"]]

app.layout = html.Div(
    [
        html.Button("Load Data", id="btn-load-data"),
        dag.AgGrid(
            id="row-selection-selected-rows",
            columnDefs=columnDefs,
            columnSize="autoSize",
            dashGridOptions={"rowSelection": "single", "animateRows": False},
        ),
        html.Pre(id="pre-row-selection-selected-rows", style={'text-wrap': 'wrap'})
    ],
)


@callback(
    Output("row-selection-selected-rows", "rowData"),
    Input("btn-load-data", "n_clicks"),
    prevent_initial_call=True,
)
def clear_table(_):
    return []


@callback(
    Output("row-selection-selected-rows", "rowTransaction"),
    Output("row-selection-selected-rows", "selectedRows"),
    Output("pre-row-selection-selected-rows", "children"),
    Input("row-selection-selected-rows", "rowData"),
    Input("row-selection-selected-rows", "selectedRows"),
    prevent_initial_call=True,
)
def output_selected_rows(rowData, selected_rows):
    print(ctx.triggered_prop_ids)
    if "row-selection-selected-rows.rowData" in ctx.triggered_prop_ids.keys():
       if not rowData: 
            return {'add': df.to_dict("records"), 'async': False}, df.head(1).to_dict("records"), ""
    else:
        selected_list = [f"{s['athlete']} ({s['year']})" for s in selected_rows]
        msg = f"You selected the athlete{'s' if len(selected_rows) > 1 else ''}:\n{', '.join(selected_list)}" if selected_rows else "No selections"
        return no_update, no_update, msg

if __name__ == "__main__":
    app.run(debug=True)

Unfortunately that suggestion does not behave as desired either. As you coded, it gives an error when the Load Data button is clicked. This is because the output_selected_rows callback is getting triggered twice and the second callback is an unexpected state where the rowData property triggered the callback but rowData isn’t empty.

I modified your suggestion as follows so that it doesn’t crash. But it still exhibits the behavior I described above. When the Load Data button is clicked for the first time, the first row isn’t shown as selected. All subsequent clicks behave as desired.

import dash_ag_grid as dag
from dash import Dash, html, Input, Output, callback, no_update, ctx
import pandas as pd

app = Dash(__name__)

df = pd.read_csv(
    "https://raw.githubusercontent.com/plotly/datasets/master/ag-grid/olympic-winners.csv"
)

columnDefs = [{"field": i} for i in ["country", "year", "athlete", "age", "sport", "total"]]

app.layout = html.Div(
    [
        html.Button("Load Data", id="btn-load-data"),
        dag.AgGrid(
            id="row-selection-selected-rows",
            columnDefs=columnDefs,
            columnSize="autoSize",
            dashGridOptions={"rowSelection": "single", "animateRows": False},
        ),
        html.Pre(id="pre-row-selection-selected-rows", style={'text-wrap': 'wrap'})
    ],
)


@callback(
    Output("row-selection-selected-rows", "rowData"),
    Input("btn-load-data", "n_clicks"),
    prevent_initial_call=True,
)
def clear_table(_):
    return []


@callback(
    Output("row-selection-selected-rows", "rowTransaction"),
    Output("row-selection-selected-rows", "selectedRows"),
    Output("pre-row-selection-selected-rows", "children"),
    Input("row-selection-selected-rows", "rowData"),
    Input("row-selection-selected-rows", "selectedRows"),
    prevent_initial_call=True,
)
def output_selected_rows(rowData, selected_rows):
    print(ctx.triggered_prop_ids)
    if rowData:
        print(rowData[:5])
    else:
        print(rowData)
    if "row-selection-selected-rows.rowData" in ctx.triggered_prop_ids.keys():
        if rowData:
            return no_update, no_update, no_update
        return {'add': df.to_dict("records"), 'async': False}, df.head(1).to_dict("records"), ""
    else:
        selected_list = [f"{s['athlete']} ({s['year']})" for s in selected_rows]
        msg = f"You selected the athlete{'s' if len(selected_rows) > 1 else ''}:\n{', '.join(selected_list)}" if selected_rows else "No selections"
        return no_update, no_update, msg

if __name__ == "__main__":
    app.run(debug=True)

Hi @adiadidas15

Try adding a unique row ID:

Here is an example


df = pd.read_csv(
    "https://raw.githubusercontent.com/plotly/datasets/master/ag-grid/olympic-winners.csv"
)
df["id"] = df.index

columnDefs = [{"field": i} for i in ["country", "year", "athlete", "age", "sport", "total"]]

app.layout = html.Div(
    [
        html.Button("Load Data", id="btn-load-data"),
        dag.AgGrid(
            id="row-selection-selected-rows",
            columnDefs=columnDefs,
            columnSize="autoSize",
            dashGridOptions={"rowSelection": "single", "animateRows": False},
            getRowId="params.data.id"
        ),
        html.Pre(id="pre-row-selection-selected-rows", style={'text-wrap': 'wrap'})
    ],
)


Try it this way:

import dash_ag_grid as dag
from dash import Dash, html, Input, Output, callback, no_update, ctx
import pandas as pd

app = Dash(__name__)

df = pd.read_csv(
    "https://raw.githubusercontent.com/plotly/datasets/master/ag-grid/olympic-winners.csv"
)

columnDefs = [{"field": i} for i in ["country", "year", "athlete", "age", "sport", "total"]]

app.layout = html.Div(
    [
        html.Button("Load Data", id="btn-load-data"),
        dag.AgGrid(
            id="row-selection-selected-rows",
            columnDefs=columnDefs,
            columnSize="autoSize",
            dashGridOptions={"rowSelection": "single", "animateRows": False},
            selectedRows=[]
        ),
        html.Pre(id="pre-row-selection-selected-rows", style={'text-wrap': 'wrap'})
    ],
)


@callback(
    Output("row-selection-selected-rows", "rowData"),
    Input("btn-load-data", "n_clicks"),
    prevent_initial_call=True,
)
def clear_table(_):
    return []


@callback(
    Output("row-selection-selected-rows", "rowTransaction"),
    Output("row-selection-selected-rows", "selectedRows"),
    Output("pre-row-selection-selected-rows", "children"),
    Input("row-selection-selected-rows", "rowData"),
    Input("row-selection-selected-rows", "selectedRows"),
    prevent_initial_call=True,
)
def output_selected_rows(rowData, selected_rows):
    print(ctx.triggered_prop_ids)
    if "row-selection-selected-rows.rowData" in ctx.triggered_prop_ids.keys():
       if not rowData:
            return {'add': df.to_dict("records"), 'async': False}, df.head(1).to_dict("records"), ""
    else:
        selected_list = [f"{s['athlete']} ({s['year']})" for s in selected_rows]
        msg = f"You selected the athlete{'s' if len(selected_rows) > 1 else ''}:\n{', '.join(selected_list)}" if selected_rows else "No selections"
        return no_update, no_update, msg
    return no_update, no_update, no_update

if __name__ == "__main__":
    app.run(debug=True)

Notice that I added selectedRows as [].

1 Like

Thanks for your help @jinnyzor! That’s works as expected!

1 Like

@jinnyzor I believe the above solution is still valid, but I wanted to share that I’ve found another edge case where row selection fails. This occurs when I add the capability for the table to resize the column widths whenever data is altered. I’ve added the corresponding code below. In this case, the first row is not shown as selected whenever the Load Data button is clicked, but clicking on rows will keep the row visibly selected after the callback is triggered.

import dash_ag_grid as dag
from dash import Dash, html, Input, Output, callback, no_update, ctx
import pandas as pd

app = Dash(__name__)

df = pd.read_csv(
    "https://raw.githubusercontent.com/plotly/datasets/master/ag-grid/olympic-winners.csv"
)

columnDefs = [{"field": i} for i in ["country", "year", "athlete", "age", "sport", "total"]]

app.layout = html.Div(
    [
        html.Button("Load Data", id="btn-load-data"),
        dag.AgGrid(
            id="row-selection-selected-rows",
            columnDefs=columnDefs,
            columnSize="autoSize",
            dashGridOptions={"rowSelection": "single", "animateRows": False},
            selectedRows=[]
        ),
        html.Pre(id="pre-row-selection-selected-rows", style={'text-wrap': 'wrap'})
    ],
)


@callback(
    Output("row-selection-selected-rows", "rowData"),
    Input("btn-load-data", "n_clicks"),
    prevent_initial_call=True,
)
def clear_table(_):
    return []


@callback(
    Output("row-selection-selected-rows", "rowTransaction"),
    Output("row-selection-selected-rows", "selectedRows"),
    Output("pre-row-selection-selected-rows", "children"),
    Input("row-selection-selected-rows", "rowData"),
    Input("row-selection-selected-rows", "selectedRows"),
    prevent_initial_call=True,
)
def output_selected_rows(rowData, selected_rows):
    print(ctx.triggered_prop_ids)
    if rowData:
        print(rowData[:5])
    else:
        print(rowData)
    if "row-selection-selected-rows.rowData" in ctx.triggered_prop_ids.keys():
        if rowData:
            return no_update, no_update, no_update
        return {'add': df.to_dict("records"), 'async': False}, df.head(1).to_dict("records"), ""
    else:
        selected_list = [f"{s['athlete']} ({s['year']})" for s in selected_rows]
        msg = f"You selected the athlete{'s' if len(selected_rows) > 1 else ''}:\n{', '.join(selected_list)}" if selected_rows else "No selections"
        return no_update, no_update, msg


@callback(
    Output("row-selection-selected-rows", "columnSize"),
    Input("row-selection-selected-rows", "rowData"),
)
def chained_col_width_update(_):
    return "autoSize"


if __name__ == "__main__":
    app.run(debug=True)

To do a chained callback like this, you shouldnt be listening to rowData, but instead be waiting for the rowTransaction to clear:

import dash_ag_grid as dag
from dash import Dash, html, Input, Output, callback, no_update, ctx
import pandas as pd

app = Dash(__name__)

df = pd.read_csv(
    "https://raw.githubusercontent.com/plotly/datasets/master/ag-grid/olympic-winners.csv"
)

columnDefs = [{"field": i} for i in ["country", "year", "athlete", "age", "sport", "total"]]

app.layout = html.Div(
    [
        html.Button("Load Data", id="btn-load-data"),
        dag.AgGrid(
            id="row-selection-selected-rows",
            columnDefs=columnDefs,
            columnSize="autoSize",
            dashGridOptions={"rowSelection": "single", "animateRows": False},
            selectedRows=[]
        ),
        html.Pre(id="pre-row-selection-selected-rows", style={'text-wrap': 'wrap'})
    ],
)


@callback(
    Output("row-selection-selected-rows", "rowData"),
    Input("btn-load-data", "n_clicks"),
    prevent_initial_call=True,
)
def clear_table(_):
    return []


@callback(
    Output("row-selection-selected-rows", "rowTransaction"),
    Output("row-selection-selected-rows", "selectedRows"),
    Output("pre-row-selection-selected-rows", "children"),
    Input("row-selection-selected-rows", "rowData"),
    Input("row-selection-selected-rows", "selectedRows"),
    prevent_initial_call=True,
)
def output_selected_rows(rowData, selected_rows):
    print(ctx.triggered_prop_ids)
    if rowData:
        print(rowData[:5])
    else:
        print(rowData)
    if "row-selection-selected-rows.rowData" in ctx.triggered_prop_ids.keys():
        if rowData:
            return no_update, no_update, no_update
        return {'add': df.to_dict("records"), 'async': False}, df.head(1).to_dict("records"), ""
    else:
        selected_list = [f"{s['athlete']} ({s['year']})" for s in selected_rows]
        msg = f"You selected the athlete{'s' if len(selected_rows) > 1 else ''}:\n{', '.join(selected_list)}" if selected_rows else "No selections"
        return no_update, no_update, msg


@callback(
    Output("row-selection-selected-rows", "columnSize"),
    Input("row-selection-selected-rows", "rowTransaction"),
)
def chained_col_width_update(_):
    if _:
        return no_update
    return "autoSize"


if __name__ == "__main__":
    app.run(debug=True)
1 Like

Thanks again @jinnyzor! That worked!

2 Likes

@jinnyzor after integrating what we worked out above into my more complicated web app, I’ve found that using rowData as a trigger was could not be tamed. I was getting several subsequent triggers that I could not isolate or catch. Instead, I’ve found that replacing the rowData trigger with a dcc.Store worked exactly as I expect. Under this callback scheme there’s no need to “catch” undesired callback triggers. So I’ve decided it is better to just avoid rowData as a trigger. This was a solution I floated in a prior post, which I’ll link here: AG Grid Row Selection Help - #6 by adiadidas15

Hi @adiadidas15

Glad you found a workaround!
But know that we are we’re working on this issue so check future releases for a fix :slight_smile:

2 Likes