Selected Rows in Dash AG Grid

Selecting Rows in Dash AG Grid

Some question have come up recently about selected rows and here is more information that has not yet made it’s way into the dash docs.

Note - Be sure to check the official dash-docs for updates.

The selectedRows prop can be set 3 ways:

  • sending the rowData of the rows you want selected
  • using row ids, for example: {"ids": ["1", "3", "5"]}
  • using a function, for example {"function": "params.data.total > '5'"}

Select Rows - using rowData

The example in the dash docs for Preslected Rows shows how to select rows by passing the rowData to the selectedRows prop. This works, but it can be slow if you want to preselect a lot of rows.

Here is an updated version that preselects the first 400 rows and uses row ids for better performance. Learn more about row ids in the dash docs. See the difference in performance by running this example both with and without this line: getRowId="params.data.id"


import dash_ag_grid as dag
from dash import Dash, html, dcc
import pandas as pd

app = Dash(__name__)


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

columnDefs = [
    {"field": "athlete", "checkboxSelection": True, "headerCheckboxSelection": True},
    {"field": "age", "maxWidth": 100},
    {"field": "country"},
    {"field": "year", "maxWidth": 120},
    {"field": "date"},
    {"field": "sport"},
    {"field": "total"},
]


defaultColDef = {"flex": 1, "minWidth": 150, "sortable": True, "resizable": True,  "filter": True}


app.layout = html.Div(
    [
        dcc.Markdown("This grid has preselected rows"),
        dag.AgGrid(
            id="preselect-checkbox-grid",
            columnDefs=columnDefs,
            rowData=df.to_dict("records"),
            defaultColDef=defaultColDef,
            dashGridOptions={"rowSelection":"multiple"},
            # preselect first 400 rows
            selectedRows= df.head(400).to_dict("records"),
            # Try commenting out the line below -- you will see a big lag before the rows are selected
             getRowId="params.data.id",
        ),
    ],
    style={"margin": 20},
)

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

Select rows using the row ids or a function

You can get even better performance by selecting rows using a list of row ids.

This example uses row ids to select the first 50 rows. It uses a function to select all rows where the year is 2012.


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

app = Dash(__name__)

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

columnDefs = [
    {"field": "athlete", "checkboxSelection": True, "headerCheckboxSelection": True},
    {"field": "age", "maxWidth": 100},
    {"field": "country"},
    {"field": "year", "maxWidth": 120},
    {"field": "date"},
    {"field": "sport"},
    {"field": "total"},
]

defaultColDef = {"flex": 1, "minWidth": 150, "sortable": True, "resizable": True,  "filter": True}

app.layout = html.Div(
    [
        html.Button("Select top 50", id="top"),
        html.Button("Select 2012", id="year"),
        dag.AgGrid(
            id="grid",
            columnDefs=columnDefs,
            rowData=df.to_dict("records"),
            defaultColDef=defaultColDef,
            dashGridOptions={"rowSelection": "multiple"},
            getRowId="params.data.id",
        ),
    ],
    style={"margin": 20},
)

@callback(
    Output("grid", "selectedRows"),
    Input("top", "n_clicks"),
    Input("year", "n_clicks"),
    prevent_intial_call=True,
)
def pre_select_rows(*_):
    if ctx.triggered_id == "top":
        # use row ids to select rows
        return {"ids": [str(i) for i in range(50)]}

    if ctx.triggered_id == "year":
        # use a function to select rows
        return {"function": "params.data.year == '2012'"}


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

ag-grid-select-rows

Select or Deselect rows using the Grid API

If you want ultimate control over the select and deselection of rows, you can use the Grid API in a clientside callback.

In this example, the user can select multiple rows, then click a button to exclude certain rows (in this case the year 2012 is deselected.

import dash_ag_grid as dag
from dash import Dash, html, Input, Output, clientside_callback
import pandas as pd

app = Dash(__name__)


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

columnDefs = [
    {"field": "athlete", "checkboxSelection": True, "headerCheckboxSelection": True},
    {"field": "age", "maxWidth": 100},
    {"field": "country"},
    {"field": "year", "maxWidth": 120},
    {"field": "date"},
    {"field": "sport"},
    {"field": "total"},
]

defaultColDef = {"flex": 1, "minWidth": 150, "sortable": True, "resizable": True, "filter": True}


app.layout = html.Div(
    [
        html.Button("Deselect 2012", id="not2012"),
        dag.AgGrid(
            id="grid",
            columnDefs=columnDefs,
            rowData=df.to_dict("records"),
            defaultColDef=defaultColDef,
            dashGridOptions={"rowSelection":"multiple"},
            getRowId="params.data.id",
        ),

    ],
    style={"margin": 20},
)

clientside_callback(
    """async function (n) {        
        gridApi = await dash_ag_grid.getApiAsync("grid")

        gridApi.forEachNode((rowNode, index) => {
            if (rowNode.data.year == '2012') {
              rowNode.setSelected(false);
            }
        });
        return dash_clientside.no_update
    }""",
    Output("grid", "id"),
    Input("not2012", "n_clicks"),
    prevent_initial_call=True
)

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

Other ways to select rows

This page from the preliminary docs has not made it the official dash-docs (yet). It shows how to select all rows or just filtered rows. Plus it shows how to select only rows on the current page. See examples here:

dag-docs

8 Likes

This is fantastic :slight_smile:

Still, if I use getRowId="params.data.id" together with clientside callback all my rows get selected. Then I pass those rows to another table:

    Output("data_table2", "rowData"),
    Output("data_table2", "columnDefs")    
    Input("data_table", "selectedRows")
...
if len(rows) > 0:
        valid_rows = [row for row in rows if isinstance(row, dict)]

        df = pd.DataFrame(valid_rows)

In this other table I can only see the last selectedRow of data_table.

This is in contrast with not using getRowId="params.data.id", and just using:

@callback(
    Output("paralells_data_table", "selectAll"),
    Input("paralells_data_table", "virtualRowData"),
    prevent_initial_call=True
)
def selectALL(rows):
    if len(rows) > 0:
        return True
...
if len(rows) > 0:
        valid_rows = [row for row in rows if isinstance(row, dict)]

        df = pd.DataFrame(valid_rows)

However, this option is not usable with large data sets because it is slower.

Is the problem the usage of aggregation here, which is not compatible with getRowId="params.data.id" when you try to pass selectedRows on? Still, I don’t understand why and how getRowId="params.data.id" actually determines how the rows will be passed on or why the rows df = pd.DataFrame(rows) returns only last row in this case.

dag.AgGrid(
    id="data_table",
    className="ag-theme-alpine",
    columnDefs=[],
    rowData=[],
    columnSize="sizeToFit",
    defaultColDef=defaultColDef_paralells,
    enableEnterpriseModules=True, 
    dashGridOptions={
                        "groupDisplayType": "singleColumn",
                        "autoGroupColumnDef": {
                                "headerName": "Paralelke",
                                "flex": 10,
                                "valueGetter": {"function": "autoGroupValueGetter2(params)"},
                                "checkboxSelection": True,
                            },
                        "rowSelection": "multiple",
                        "suppressAggFuncInHeader": True,
                        "groupSelectsChildren": True,
                        "s

On another matter, how could someone call gridAPI from dagfuncs = window.dashAgGridFunctions? I’m looking for a way to return getSelectedRows() and run custom aggfunction on them.

Hi @davzup89

I’m not sure I understand what’s going on with your code. Could you make a complete minimal example?

You can access the grid api from a clientside callback - you can find an example in this post: Dash AG-Grid: Export Data As Excel - #5 by AnnMarieW

If you are trying to access the grid api from a function defined in the column defs like in your post about custom aggregation functions, it’s available in the params passed to the function. To see everything that’s available adding :

dagfuncs.customAvgAggFunc = function(params) {
   console.log(params)
1 Like

Great, thank you for the suggestion.

My code problem comes down to setting id as index. As I haven’t done that explicitly for my df, only last selected row gets passed with selectedRows.

Can you make a complete minimal example that replicates the issue?

This is the example, but as said, the problem is there only if id is not set as index (commented out in below example)

import dash_ag_grid as dag
from dash import Dash, html, Input, Output, clientside_callback
import pandas as pd

app = Dash(__name__)


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

columnDefs = [
    {"field": "athlete", "checkboxSelection": True, "headerCheckboxSelection": True},
    {"field": "age", "maxWidth": 100},
    {"field": "country"},
    {"field": "year", "maxWidth": 120},
    {"field": "date"},
    {"field": "sport"},
    {"field": "total"},
]

defaultColDef = {"flex": 1, "minWidth": 150, "sortable": True, "resizable": True, "filter": True}

table1 = dag.AgGrid(
            id="grid1",
            columnDefs=columnDefs,
            rowData=df.to_dict("records"),
            defaultColDef=defaultColDef,
            dashGridOptions={"rowSelection":"multiple"},
            getRowId="params.data.id",
        )

table2 = dag.AgGrid(
            id="grid2",
            columnDefs=columnDefs,
            rowData=[],
            defaultColDef=defaultColDef,
            dashGridOptions={"rowSelection":"multiple"},
            getRowId="params.data.id",
        )

app.layout = html.Div(
    [
        html.Button("Select 2012", id="button"),
        table1,
        table2
    ],
    style={"margin": 20},
)

@app.callback(
    Output("grid2", "rowData"),
    Input("grid1", "selectedRows"),
    prevent_initial_call=True
)
def prepare_dissolution_tbl_data(rows):
    if len(rows) == 0:
        return None
    if len(rows) > 0:
        valid_rows = [row for row in rows if isinstance(row, dict)]

        df = pd.DataFrame(valid_rows)
                     

    return  df.to_dict("records")


clientside_callback(
    """async function (n) {        
        gridApi = await dash_ag_grid.getApiAsync("grid1")
    
        gridApi.forEachNode((rowNode, index) => {
            rowNode.setSelected(true);
        });
    
        return dash_clientside.no_update
    }""",
    Output("grid1", "id"),
    Input("button", "n_clicks"),
    prevent_initial_call=True
)

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

Hello @davzup89,

You are telling the grid to use something as an unique identifier that does not exist. You can either assign the id as the index (uncommenting the df[‘id’] bit) or comment out where you set the ids.

The grid is actually failing to update how visually how your selections are working. Unless you are holding ctrl when clicking the rows, or selecting inside the checkboxes, you are only setting one selection at a time.

Another thing of note, you dont need to convert to a df and back to an array of dictionaries, just use this function:

@app.callback(
    Output("grid2", "rowData"),
    Input("grid1", "selectedRows"),
    prevent_initial_call=True
)
def prepare_dissolution_tbl_data(rows):
    if len(rows) == 0:
        return None
    if len(rows) > 0:
        valid_rows = [row for row in rows if isinstance(row, dict)]
        return valid_rows

    return no_update
1 Like