Dash ag grid cellValueChanged updates one step behind when used with clientside callback

Hi all,

I made a table which updates pretty much the entire table when it recon the changes within a cell.
Using Patch(), it can read and write data just fine, but it was too slow and decided to implement clientside_callback.

And I noticed that it doesn’t really work as expected,
the changes are noticeably faster but they are just one execution behind.

I’ve added an example incase my words weren’t clear enough.
It was tested on py3.9.13, dash==2.11.1, dash-ag-grid==2.3.0

from copy import deepcopy

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

app = Dash(__name__)

# df init
df = pd.DataFrame(np.random.randint(0, 10, size=(15, 2)), columns=list("ab"))
df['c'] = df.a + df.b
df.reset_index(inplace=True)

columnDefs = [
    {
        "headerName": "Editable A",
        "field": "a",
        "editable": True,
        "resizable": True,
    },
    {
        "headerName": "Editable B",
        "field": "b",
        "editable": True,
    },
    {
        "headerName": "A + B",
        "field": "c",
    }
]

columnDefsAnimation = deepcopy(columnDefs)
columnDefsAnimation[2]["cellRenderer"] = "agAnimateShowChangeCellRenderer"

defaultColDef = {
    "filter": "agNumberColumnFilter",
    "resizable": True,
    "sortable": True,
}

app.layout = html.Div(
    [
        # w/ renderer
        html.H3("With animation renderer"),
        html.Div(
            dag.AgGrid(
                id="animation-grid",
                columnDefs=columnDefsAnimation,
                rowData=df.to_dict("records"),
                columnSize="sizeToFit",
                defaultColDef=defaultColDef,
                # setting a row ID is required when updating data in a callback
                getRowId="params.data.index",
            ),
        ),
        # w/o renderer
        html.H3("Without animation renderer"),
        html.Div(
            dag.AgGrid(
                id="no-animation-grid",
                columnDefs=columnDefs,
                rowData=df.to_dict("records"),
                columnSize="sizeToFit",
                defaultColDef=defaultColDef,
                # setting a row ID is required when updating data in a callback
                getRowId="params.data.index",
            ),
        ),
    ],
    style={"margin": 20},
)

clientside_callback(
    """
    function(cellValueChanged, rowData) {
        let newRowData = rowData
        if (cellValueChanged, rowData) {
            newRowData[cellValueChanged.rowIndex]['c'] =
                parseFloat(newRowData[cellValueChanged.rowIndex]['a']) +
                parseFloat(newRowData[cellValueChanged.rowIndex]['b']);
            return [newRowData]
        }
        return window.dash_clientside.no_update;
    }
    """,
    [Output("no-animation-grid", "rowData")],
    [Input("no-animation-grid", "cellValueChanged")],
    [State("no-animation-grid", "rowData")],
    prevent_initial_call=True
)
clientside_callback(
    """
    function(cellValueChanged, rowData) {
        let newRowData = rowData
        if (cellValueChanged, rowData) {
            newRowData[cellValueChanged.rowIndex]['c'] =
                parseFloat(newRowData[cellValueChanged.rowIndex]['a']) +
                parseFloat(newRowData[cellValueChanged.rowIndex]['b']);
            return [newRowData]
        }
        return window.dash_clientside.no_update;
    }
    """,
    [Output("animation-grid", "rowData")],
    [Input("animation-grid", "cellValueChanged")],
    [State("animation-grid", "rowData")],
    prevent_initial_call=True
)

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

There is high chance that I might be missing something :slight_smile:

Thanks.

Hello @jhmin,

Welcome to the community!

This is interesting for sure. I’ll take a look at why what you are doing isn’t working. The rowData is the issue here for what you are trying to do.

With that said however, all of the data that you need is inside of the cellValueChanged. You should be updating the c value with cellValueChanged.data[‘a’] + cellValueChanged.data[‘b’].

Assuming you are needing to store this data, this would be the way I would do it… but… if you aren’t. Then you should be using a valueGetter for column c that is set up to add the two columns. No callback needed.

Hi @jinnyzor,
thanks for the reply.

Yup, what I want to achieve is not exactly the same as an example.
Example won’t really need the whole rowData on use, but for my used case the data are correlated column and row-wised, so in the end I need to update the entire data.

The entire new set of data is in the cellValueChanged.data, for the row.

I will take a look at your example in a bit and see what is causing the issue with the rowData.

1 Like

Hello @jhmin,

So the issue here is not the rowData exactly, its the fact that you were altering the object in place, when sent back to the grid, it didnt know that it was new. This actually happens on quite a few things in Dash, mainly dcc.Graph figures.

Anyways, here is a working version:

from copy import deepcopy

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

app = Dash(__name__)

# df init
df = pd.DataFrame(np.random.randint(0, 10, size=(15, 2)), columns=list("ab"))
df['c'] = df.a + df.b
df.reset_index(inplace=True)

columnDefs = [
    {
        "headerName": "Editable A",
        "field": "a",
        "editable": True,
        "resizable": True,
    },
    {
        "headerName": "Editable B",
        "field": "b",
        "editable": True,
    },
    {
        "headerName": "A + B",
        "field": "c",
    }
]

columnDefsAnimation = deepcopy(columnDefs)
columnDefsAnimation[2]["cellRenderer"] = "agAnimateShowChangeCellRenderer"

defaultColDef = {
    "filter": "agNumberColumnFilter",
    "resizable": True,
    "sortable": True,
}

app.layout = html.Div(
    [
        # w/ renderer
        html.H3("With animation renderer"),
        html.Div(
            dag.AgGrid(
                id="animation-grid",
                columnDefs=columnDefsAnimation,
                rowData=df.to_dict("records"),
                columnSize="sizeToFit",
                defaultColDef=defaultColDef,
                # setting a row ID is required when updating data in a callback
                getRowId="params.data.index",
            ),
        ),
        # w/o renderer
        html.H3("Without animation renderer"),
        html.Div(
            dag.AgGrid(
                id="no-animation-grid",
                columnDefs=columnDefs,
                rowData=df.to_dict("records"),
                columnSize="sizeToFit",
                defaultColDef=defaultColDef,
                # setting a row ID is required when updating data in a callback
                getRowId="params.data.index",
            ),
        ),
    ],
    style={"margin": 20},
)

clientside_callback(
    """
    function(cellValueChanged, rowData) {
        let newRowData = JSON.parse(JSON.stringify(rowData))
        if (cellValueChanged, rowData) {
            newRowData[cellValueChanged.rowIndex]['c'] =
                parseFloat(cellValueChanged.data.a) +
                parseFloat(cellValueChanged.data.b);
            return [newRowData]
        }
        return window.dash_clientside.no_update;
    }
    """,
    [Output("no-animation-grid", "rowData")],
    [Input("no-animation-grid", "cellValueChanged")],
    [State("no-animation-grid", "rowData")],
    prevent_initial_call=True
)
clientside_callback(
    """
    function(cellValueChanged, rowData) {
        let newRowData = JSON.parse(JSON.stringify(rowData))
        if (cellValueChanged, rowData) {
            newRowData[cellValueChanged.rowIndex]['c'] =
                parseFloat(cellValueChanged.data.a) +
                parseFloat(cellValueChanged.data.b);
            return [newRowData]
        }
        return window.dash_clientside.no_update;
    }
    """,
    [Output("animation-grid", "rowData")],
    [Input("animation-grid", "cellValueChanged")],
    [State("animation-grid", "rowData")],
    prevent_initial_call=True
)

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

Thanks a lot!

It also works like a gem on my practical table.

Many thanks :smiley:

1 Like