Dash AG Grid - Infinite Row Model - Updating startRow and endRow

Hey guys,
I am using infinite row model for my table because my table is quite large (100k+ rows). In simple example it works like a charm BUT there is a catch!

I have a fancy UI for filtering the dataframe which have to be involved. In simple pseudocode my callback looks like this:

@app.callback(
    Output("my-table", "getRowsResponse"),
    Input("my-table", "getRowsRequest"),
    Input("fire_filters", "n_clicks"),
    State("filters", "value"),
)
def update_table(request, n_clicks, filters_data)
    filtered_df = # here I filter my df using filters

    partial = filtered_df .iloc[request["startRow"] : request["endRow"]]
    return {"rowData": partial.to_dict("records"), "rowCount": len(filtered_df .index)}

Problem occurs i.e. in this example:

  • Initially I filter in my externals filters dataframe with 300 rows, table is initialized correctly
  • I scroll down, startRow 100 and endRow 200 are provided, table is updated correctly
  • Now I change my external filters to data with only 70 rows, I hit the fire filters button. After hitting the button startRow 100 and endRow 200 are provided.

I would like to work with startRow 100 and endRow 200. So using ctx when fire_filters is triggered I use it through my callback with values I need. Everything in callback runs correctly, partial looks as it should but rowData does not get updated. Interestlingly rowCount is updated to 70 so I see 70 rows but with the previous data.

I believe that problem is that startRow and endRow are not updated but I have no idea how to do that. I even tried to use scrollTo and the table scrolls to row 0 but it has no impact. Any other triggering the callback takes the same values 100 for startRow and 200 for endRow.

Any help would be appreciated :slight_smile: Thanks

1 Like

Hello @martin2097,

It really helps if you can give a complete MRE. :wink:

Try this:

@app.callback(
    Output("my-table", "getRowsResponse"),
    Input("my-table", "getRowsRequest"),
    Input("fire_filters", "n_clicks"),
    Input("filters", "value"),
)
def update_table(request, n_clicks, filters_data)
    if ctx.triggered_id == 'filters':
        return {'rowData': [], 'rowCount': 1}
    filtered_df = # here I filter my df using filters

    partial = filtered_df .iloc[request["startRow"] : request["endRow"]]
    return {"rowData": partial.to_dict("records"), "rowCount": len(filtered_df .index)}

It does not work but I built a MRE based on one of your codes in a meantime :slight_smile:

from dash import Dash, html, Input, Output, no_update, State
from dash_ag_grid import AgGrid
import dash_mantine_components as dmc
import pandas as pd


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

# basic columns definition with column defaults
columnDefs = [{"field": c} for c in df.columns]

app.layout = html.Div(
    [
        AgGrid(
            id="grid",
            columnDefs=columnDefs,
            rowData=df.to_dict("records"),
            defaultColDef={"resizable": True, "sortable": True, "filter": True},
            rowModelType="infinite",
            dashGridOptions={
                "pagination": True
            },
        ),
        dmc.ChipGroup(
            [dmc.Chip(x, value=x) for x in ["United States", "Afghanistan"]],
            value="United States",
            id="filters",
        ),
        html.Button(id="fire-filters", children="Fire Filters"),
    ]
)


@app.callback(
    Output("grid", "getRowsResponse"),
    Input("grid", "getRowsRequest"),
    Input("fire-filters", "n_clicks"),
    State("filters", "value"),
)
def update_table(request, n_clicks, filters_data):
    if request is None:
        return no_update
    print(request)
    filtered_df = df[df['country'] == filters_data]

    partial = filtered_df.iloc[request["startRow"]: request["endRow"]]
    return {"rowData": partial.to_dict("records"), "rowCount": len(filtered_df.index)}


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

How to break it:

  1. Go to second page
  2. Select Afghanistan
  3. Hit fire filters

You should see 2 rows (correct count) but showing USA athletes.

I used pagination because it is more obvious what is going on. It behaves the same without pagination. I believe problem is at not updated startRow and endRow

Try this instead:

from dash import Dash, html, Input, Output, no_update, State, ctx
from dash_ag_grid import AgGrid
import dash_mantine_components as dmc
import pandas as pd


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

# basic columns definition with column defaults
columnDefs = [{"field": c} for c in df.columns]

app.layout = html.Div(
    [
        AgGrid(
            id="grid",
            columnDefs=columnDefs,
            rowData=df.to_dict("records"),
            defaultColDef={"resizable": True, "sortable": True, "filter": True},
            rowModelType="infinite",
            dashGridOptions={
                "pagination": True
            },
        ),
        dmc.ChipGroup(
            [dmc.Chip(x, value=x) for x in ["United States", "Afghanistan"]],
            value="United States",
            id="filters",
        ),
        html.Button(id="fire-filters", children="Fire Filters"),
    ]
)


@app.callback(
    Output("grid", "getRowsResponse"),
    Input("grid", "getRowsRequest"),
    State("filters", "value"),
)
def update_grid(request, filters_data):
    if request:
        filtered_df = df[df['country'] == filters_data]

        partial = filtered_df.iloc[request["startRow"]: request["endRow"]]
        return {"rowData": partial.to_dict("records"), "rowCount": len(filtered_df.index)}

@app.callback(
    Output("grid", "getRowsRequest"),
    Input("fire-filters", "n_clicks"),
    prevent_initial_call=True
)
def reset_grid(n_clicks):
    return {"startRow": 0, "endRow": 100}


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

Thanks for quick response. Well, it seems like startRow and endRow is updating, partial has a correct data but it seems like grid does not update rowData :confused: So the two rows are still from USA.

rowData isnt used on infinite row model. :stuck_out_tongue:

If you want the rowData to update as well, you’ll need to pass the portions that you want to that prop.

what I meant was this part of return, it seems like grid don’t takes it in this case.

rowData in that sense is what is given to display the info in the grid… I’m confused, because my grid works properly…

What version are you using?

Okay that is odd, when I repeat the steps I provided it is still broken for me.

I updated dash-ag-grid today to 2.2.0
My dash version is 2.11.0

Mine works in those:

This works fine for me:

  1. Open new app
  2. Select Afghanistan
  3. Fire filters

But this don’t

  1. Open new app
  2. Go to second page
  3. Select Afghanistan
  4. Hit fire filters

I am going to try it on another computer if it might be related to some venv stuff.

Also, make sure you are using my example and not yours…

Wait… I see the issue now on the pages… hmm…

1 Like

Here you go, using the api:

from dash import Dash, html, Input, Output, no_update, State, ctx
from dash_ag_grid import AgGrid
import dash_mantine_components as dmc
import pandas as pd


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

# basic columns definition with column defaults
columnDefs = [{"field": c} for c in df.columns]

app.layout = html.Div(
    [
        AgGrid(
            id="grid",
            columnDefs=columnDefs,
            defaultColDef={"resizable": True, "sortable": True, "filter": True},
            rowModelType="infinite",
            dashGridOptions={
                "pagination": True
            },
        ),
        dmc.ChipGroup(
            [dmc.Chip(x, value=x) for x in ["United States", "Afghanistan"]],
            value="United States",
            id="filters",
        ),
        html.Button(id="fire-filters", children="Fire Filters"),
    ]
)


@app.callback(
    Output("grid", "getRowsResponse"),
    Input("grid", "getRowsRequest"),
    State("filters", "value"),
)
def update_grid(request, filters_data):
    if request:
        filtered_df = df[df['country'] == filters_data]

        partial = filtered_df.iloc[request["startRow"]: request["endRow"]]
        return {"rowData": partial.to_dict("records"), "rowCount": len(filtered_df.index)}

app.clientside_callback(
    """function (n) {
        dash_ag_grid.getApi('grid').purgeInfiniteCache()
        return dash_clientside.no_update
    }""",
    Output("fire-filters", "n_clicks"),
    Input("fire-filters", "n_clicks"),
    prevent_initial_call=True
)


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

More info from here:

1 Like

Thank you so much! It looks like it works on provided MRE :slight_smile: I will update my main app and let you know.

BTW is this possible thanks to yesterdays update to 2.2.0?

Yes, access to the api was granted with the release of 2.2.0 :slight_smile:

Check out more cool things here:

I was just curious. Well timed issue thanks to my procrastination :smiley:

1 Like

You could have also just set it up on different tabs. :wink: Using dbc tabs it would give some freedom.

Thanks @jinnyzor for pointing into right direction. Seeing what you did there (purging Cache) maked me wonder if it is not solvable using maxBlocksInCache grid option. And it is! Combining your first two callback idea with maxBlocksInCache results in this:

from dash import Dash, html, Input, Output, no_update, State, ctx
from dash_ag_grid import AgGrid
import dash_mantine_components as dmc
import pandas as pd


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

# basic columns definition with column defaults
columnDefs = [{"field": c} for c in df.columns]

app.layout = html.Div(
    [
        AgGrid(
            id="grid",
            columnDefs=columnDefs,
            rowData=df.to_dict("records"),
            defaultColDef={"resizable": True, "sortable": True, "filter": True},
            rowModelType="infinite",
            dashGridOptions={
                "pagination": True,
                "maxBlocksInCache": 1,
            },
        ),
        dmc.ChipGroup(
            [dmc.Chip(x, value=x) for x in ["United States", "Afghanistan"]],
            value="United States",
            id="filters",
        ),
        html.Button(id="fire-filters", children="Fire Filters"),
    ]
)


@app.callback(
    Output("grid", "getRowsResponse"),
    Input("grid", "getRowsRequest"),
    State("filters", "value"),
)
def update_grid(request, filters_data):
    if request:
        filtered_df = df[df['country'] == filters_data]

        partial = filtered_df.iloc[request["startRow"]: request["endRow"]]
        return {"rowData": partial.to_dict("records"), "rowCount": len(filtered_df.index)}

@app.callback(
    Output("grid", "getRowsRequest"),
    Input("fire-filters", "n_clicks"),
    prevent_initial_call=True
)
def reset_grid(n_clicks):
    return {"startRow": 0, "endRow": 100}


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

I believe this is the simplest solution :slight_smile:

2 Likes

If it works, lol.

I wouldnt recommend this to be paired without pagination.

I think if you have people scrolling through pages quickly, there could be a performance hit to go back to a previously viewed page.

1 Like