Update Dash Ag Grid data without refreshing the data

Hello guys,

I have some dynamic cells in my DASH AG GRID, which I would like to update some fields based on others;

It is working as expected but one issue I’m facing is that once it sends the updated data it scrolls up the table to the top

update-issue

I would like to update the data fields but keep the table in that specific field without reloading it and scrolling up

Below is an MRE:

import dash
from dash import html, Dash, Output, Input, State, dcc
import dash_ag_grid as dag
import pandas as pd
import dash_mantine_components as dmc

list_of_vals = [
    {
        "Val ID": "Item 001",
        "Year Col": "2017",        
        "Text Col": "testing a random text which is long enough to wrap",
        "Bool Col": "No",
        "Options Col": "OTHER OPTION 2",
        "Long List Col": "option 1",
        "Country Col": "Brazil",
        "Date Col": "2024-01-01",
        "Hours Col": 1253.0,
        "Value Col": None,
    },
    {
        "Val ID": "Item 002",
        "Year Col": "2016",
        "Text Col": "testing a random text which is long enough to wrap",
        "Bool Col": "Yes",
        "Options Col": "OTHER OPTION 3",
        "Long List Col": "option 2",
        "Country Col": "France",
        "Date Col": "2024-01-01",
        "Hours Col": 2541.0,
        "Value Col": None
    },
    {
        "Val ID": "Item 003",
        "Year Col": "2019",      
        "Text Col": "testing a random text which is long enough to wrap",
        "Bool Col": "Yes",
        "Options Col": "OTHER OPTION 3",
        "Long List Col": "option 5",
        "Country Col": "China",
        "Date Col": "2024-01-01",
        "Hours Col": 566.0,
        "Value Col": None
    },
    {
        "Val ID": "Item 004",
        "Year Col": "2013",      
        "Text Col": "testing a random text which is long enough to wrap",
        "Bool Col": "No",
        "Options Col": "OTHER OPTION 1",
        "Long List Col": "option 12",
        "Country Col": "China",
        "Date Col": "2024-01-01",
        "Hours Col": 2732.0,
        "Value Col": None
    },
    {
        "Val ID": "Item 005",
        "Year Col": "2018",      
        "Text Col": "testing a random text which is long enough to wrap",
        "Bool Col": "No",
        "Options Col": "OTHER OPTION 2",
        "Long List Col": "option 8",
        "Country Col": "France",
        "Date Col": "2024-01-01",
        "Hours Col": 1543.0,
        "Value Col": None
    },
    {
        "Val ID": "Item 006",
        "Year Col": "2011",      
        "Text Col": "testing a random text which is long enough to wrap",
        "Bool Col": "Yes",
        "Options Col": "OTHER OPTION 6",
        "Long List Col": "option 7",
        "Country Col": "Brazil",
        "Date Col": "2024-01-01",
        "Hours Col": 1543.0,
        "Value Col": None
    },
    {
        "Val ID": "Item 007",
        "Year Col": "2012",      
        "Text Col": "testing a random text which is long enough to wrap",
        "Bool Col": "Yes",
        "Options Col": "OTHER OPTION 4",
        "Long List Col": "option 10",
        "Country Col": "Italy",
        "Date Col": "2024-01-01",
        "Hours Col": 1543.0,
        "Value Col": None
    },
    {
        "Val ID": "Item 008",
        "Year Col": "2017",      
        "Text Col": "testing a random text which is long enough to wrap",
        "Bool Col": "Yes",
        "Options Col": "OTHER OPTION 6",
        "Long List Col": "option 9",
        "Country Col": "France",
        "Date Col": "2024-01-01",
        "Hours Col": 1543.0,
        "Value Col": None
    },
    {
        "Val ID": "Item 009",
        "Year Col": "2016",      
        "Text Col": "testing a random text which is long enough to wrap",
        "Bool Col": "Yes",
        "Options Col": "OTHER OPTION 4",
        "Long List Col": "option 14",
        "Country Col": "China",
        "Date Col": "2024-01-01",
        "Hours Col": 1543.0,
        "Value Col": None
    },
    {
        "Val ID": "Item 010",
        "Year Col": "2016",      
        "Text Col": "testing a random text which is long enough to wrap",
        "Bool Col": "No",
        "Options Col": "OTHER OPTION 3",
        "Long List Col": "option 14",
        "Country Col": "Germany",
        "Date Col": "2024-01-01",
        "Hours Col": 1543.0,
        "Value Col": None
    },
    {
        "Val ID": "Item 011",
        "Year Col": "2012",      
        "Text Col": "testing a random text which is long enough to wrap",
        "Bool Col": "No",
        "Options Col": "OTHER OPTION 9",
        "Long List Col": "option 8",
        "Country Col": "France",
        "Date Col": "2024-01-01",
        "Hours Col": 1543.0,
        "Value Col": None
    },
    {
        "Val ID": "Item 012",
        "Year Col": "2020",      
        "Text Col": "testing a random text which is long enough to wrap",
        "Bool Col": "Yes",
        "Options Col": "OTHER OPTION 7",
        "Long List Col": "option 35",
        "Country Col": "Germany",
        "Date Col": "2024-01-01",
        "Hours Col": 1543.0,
        "Value Col": None
    },
]

for val in list_of_vals:
    val["Value Col"] = val["Hours Col"]*30

df = pd.DataFrame(list_of_vals)
df = df.set_index("Val ID").T
df = df.reset_index().rename(columns={"index": "Val ID"})
data_list = [item for item in df.to_dict(orient="records")]
columns_list = list(df.columns)

app = Dash()

def get_country_values():
    return {"long_list_options": [f"option {n}" for n in range(2000)], "country":[
        "Afghanistan",
        "Aland Islands",
        "Albania",
        "Algeria",
        "France", 
        "Brazil", 
        "China", 
        "Germany",
        "Italy",
        "Christmas Island",
        "Cocos (Keeling) Islands",
        "Tahiti",
        "TBD",
    ]}


app.layout = html.Div(
    [
        dcc.Store(id="changed-cell", data=[]),
        html.H2("Dash AG GRID - Rows cell styling"),
        dag.AgGrid(
            rowData=data_list,
            columnDefs=[{"field": "Val ID", "editable": False}]
            + [
                {
                    "field": c,
                    "cellRenderer": "FormatSpecificCells",
                    "valueFormatter": {"function": "FormatNumbersByRow(params)"},
                    "cellEditorSelector": {
                        "function": f"cellEditorSelector(params)"
                    },
                    "cellDataType": False,
                    "minWidth": 80,
                    "wrapText": True,
                    "cellEditor": {"function": "DMC_Select"},
                    "cellEditorParams": {
                        "options": get_country_values(),
                        "clearable": True,
                        "placeholder": "Select Option",
                    },
                    "autoHeight": True,
                    "valueSetter": {"function": "testValue(params)"},
                    "editable": {
                        "function": "['Value Col'].indexOf(params.data['Val ID']) === -1"
                    },
                }
                for c in columns_list
                if c != "Val ID"
            ],
            defaultColDef={"editable": True},
            dashGridOptions={
                "undoRedoCellEditing": False,
                "undoRedoCellEditingLimit": 20,
            },
            id="ag-grid-table",
            style={"height": "300px"},
        ),
        dcc.Store(id="original-data", data=data_list),
        html.Div(id="edited-table-output"),
        html.Div(id="output"),
    ],
    style={"padding": "16px"},
)


@app.callback(
    Output("changed-cell", "data", allow_duplicate=True),
    Input("ag-grid-table", "cellValueChanged"),
    Input("ag-grid-table", "rowData"),
    # Input("ag-grid-table", "active_cell"),
    State("changed-cell", "data"),
    State("original-data", "data"),
    prevent_initial_call=True,
)
def update_table(changed_value, data_table, changes_stored, og_data):  # active_cell,

    triggered_id = [t["prop_id"] for t in dash.callback_context.triggered][0]

    if changed_value is not None:

        key = changed_value[0]["colId"]

        changed_column = changed_value[0]["data"]["Val ID"]
        row_index = changed_value[0]["rowId"]
        new_value = changed_value[0]["value"]
        change_id = f"{key}-{changed_column}"

        for row in og_data:
            if row["Val ID"] == changed_column:
                old_value = row[key]
            else:
                pass
        # Test if the old and new values are the same
        if new_value == old_value:
            changes_stored = [
                changes_stored
                for changes_stored in changes_stored
                if changes_stored["change_id"] != change_id
            ]
            return changes_stored

        new_changes = {
            "change_id": change_id,
            "val-ID": key,
            "index_row": row_index,
            "column": changed_column,
            "old_value": old_value,
            "new_value": new_value,
        }

        changes_stored = [
            changes_stored
            for changes_stored in changes_stored
            if changes_stored["change_id"] != change_id
        ]
        changes_stored.append(new_changes)

        return changes_stored

    return dash.no_update


def gen_update_row_btn(changes):
    return html.Div(
        [
            html.Div(
                [
                    html.Div(
                        [html.Div(f"val-ID: "), html.B(changes["val-ID"])],
                        style={"display": "flex", "gap": "8px", "marginRight": "16px"},
                    ),
                    html.Div(
                        [html.Div(f"Changed Column: "), html.B(changes["column"])],
                        style={"display": "flex", "gap": "8px", "marginRight": "16px"},
                    ),
                    html.Div(
                        [
                            html.Div(f"Old Value & Type: "),
                            html.B(
                                f'{changes["old_value"]} - {type(changes["old_value"])}'
                            ),
                        ],
                        style={"display": "flex", "gap": "8px", "marginRight": "16px"},
                    ),
                    html.Div(
                        [
                            html.Div(f"New Value & Type: "),
                            html.B(
                                f'{changes["new_value"]} - {type(changes["new_value"])}'
                            ),
                        ],
                        style={"display": "flex", "gap": "8px", "marginRight": "16px"},
                    ),
                ],
                style={"display": "flex"},
            ),
        ],
        className="",
    )


@app.callback(Output("edited-table-output", "children"), 
              Input("changed-cell", "data"))
def update_d(cc_data):
    if cc_data == []:
        return None
    else:
        list_of_changes_to_confirm = [gen_update_row_btn(change) for change in cc_data]
        if len(cc_data) == 0:
            return None
        elif len(cc_data) == 1:
            return html.Div(
                [
                    html.Div(list_of_changes_to_confirm),
                    html.Div(
                        html.Button(
                            "Confirm Changes",
                            id={
                                "type": "confirm-all-btn",
                                "id": "test",
                            },
                            className="",
                        ),
                        className="textCenter bottom16",
                    ),
                ]
            )
        else:
            return html.Div(
                [
                    html.Div(list_of_changes_to_confirm),
                    html.Div(
                        html.Button(
                            "Confirm Changes",
                            id={
                                "type": "confirm-all-btn",
                                "id": "test",
                            },
                            className="",
                        ),
                        className="textCenter bottom16",
                    ),
                ]
            )


@app.callback(
    Output("ag-grid-table", "columnDefs"),
    Input("changed-cell", "data"),
    State("ag-grid-table", "columnDefs"),
)
def update_content_output(changed_cells, columns):
    key_to_check = "cellStyle"
    if changed_cells == []:
        to_change_cells = 0
        for (n, val) in enumerate(columns):
            if key_to_check in val.keys():
                if val["cellStyle"] == {}:
                    pass
                else:
                    to_change_cells += 1
                    val["cellStyle"] = {}
            else:
                pass
        if to_change_cells == 0:
            return dash.no_update
        else:
            return columns

    list_of_sn = [val["val-ID"] for val in changed_cells]

    for (n, val) in enumerate(columns):
        if val["field"] in list_of_sn:
            sn_val = val["field"]
            list_of_rows = [
                int(get_rows["index_row"])
                for get_rows in changed_cells
                if get_rows["val-ID"] == sn_val
            ]
            cellStyle = {
                "styleConditions": [
                    {
                        "condition": f"[{','.join(map(str, list_of_rows))}].includes(params.node.childIndex)",
                        "style": {"backgroundColor": "yellow"},
                    },
                    {
                        "condition": f"![{','.join(map(str, list_of_rows))}].includes(params.node.childIndex)",
                        "style": {},
                    },
                ]
            }
            val["cellStyle"] = cellStyle
        else:
            if key_to_check in val.keys():
                if val["cellStyle"] != {}:
                    val["cellStyle"] = {}
                    # else:
                    #     pass
                else:
                    pass
            else:
                pass
    return columns

@app.callback(
    Output("ag-grid-table", "rowData"),
    Input("ag-grid-table", "cellValueChanged"),
    State("ag-grid-table", "rowData"),
)
def update_rowdata(change, full_data):
    if change is None:
        return dash.no_update
    
    column=change[0]['data']['Val ID']
    change_val_id=change[0]['colId']
    
    if column != "Hours Col":
        return dash.no_update
    new_value_hours=change[0]['value']
    row_index={val["Val ID"]:n for (n, val) in enumerate(full_data)}
    value_col_index=row_index["Value Col"]
    full_data[value_col_index][change_val_id]=new_value_hours*30

    return full_data


if __name__ == "__main__":
    app.run_server(debug=True, port=8050)

on your assets folder you need to add agGridFunctions.js

var dagfuncs = (window.dashAgGridFunctions = window.dashAgGridFunctions || {});

dagfuncs.Intl = Intl;

dagfuncs.FormatNumbersByRow = function(params) {
   if (isNaN(params.value)) {
        return params.value;
    }
    if (params.data["Val ID"] == "Hours Col") {
        return Intl.NumberFormat("en-US").format(params.value);
    }
    if (params.data["Val ID"] == "Value Col") {
        return Intl.NumberFormat("en-US", {
            style: "currency",
            currency: "USD",
            minimumFractionDigits: 0,
            maximumFractionDigits: 0,
          }).format(params.value);
    }
}

dagfuncs.cellEditorSelector = function(params, list_vals) {
    
    if (["Text Col", "Year Col"].includes(params.data["Val ID"]))  {
        return {
            component: "agTextCellEditor",
        }
    };
    if (["Date Col"].includes(params.data["Val ID"]))  {
        return {
            component: "agDateStringCellEditor",
            params: {
                'useFormatter': true,
                'min': '2023-01-01',
                'max': '2040-12-31',
              },
        }
    };
    if (["Hours Col", "Value Col"].includes(params.data["Val ID"])) {
        return {
            component: "agNumberCellEditor",
        }
    };
    if (["Bool Col"].includes(params.data["Val ID"])) {
        return {
          component: 'agSelectCellEditor',
          params: {
            values: ['Yes', 'No'],
          },
          popup: true,
        };
    };
    if (["Options Col"].includes(params.data["Val ID"])) {
        return {
          component: 'agSelectCellEditor',
          params: {
            values: ['Option 1', 'Option 2', 'Other Option 1', 
                     'Other Option 2', '3'],
          },
        };
    };
    return undefined;
};

function isFourDigits(value) {
    let fourDigitsRegex = /^\d{4}$/;
    return fourDigitsRegex.test(value);
}
function isBetween1950AndCurrentYear(value) {
    let currentYear = new Date().getFullYear();
    return value >= 1950 && value <= currentYear;
}

dagfuncs.testValue = (params) => {
    if (!params.newValue) {
        return false
    }
    if (params.data["Val ID"] == "Year Col") {
        let currentYear = new Date().getFullYear();
        // Check if the text is composed by 4 digits and the YEAR is between 1900 and 2022 
        if (!isFourDigits(params.newValue)) {
            return false
        } 
        if (!isBetween1950AndCurrentYear(params.newValue)) {
            return false
        }
    }  
    params.data[params.column.colId] = params.newValue
    return true
}



I’ve discovered the rowTransaction alternative but after some tests, I didn’t understand how I could use it on my current code;

If you have any question please let me know; Any help on this will be very welcome

Hi @kabure

Try adding the getRowId prop

2 Likes

Hello @AnnMarieW Thank you so much for the answer;

I did use it before but didn’t work, but I tried it again and now it worked lol

In the context of my example I added:
getRowId="params.data['Val ID']"

It initially broke my structure but after some adjusts it is now working exactly as expected! Thank you again dear!

1 Like