How to handle multiple dependent columns on Dash Ag Grid

I recently had to build a Dash Ag Grid where multiple columns interact i.e. changing Width or Length changes the calculated Area and changing Area changes either Width or Length correspondingly. This was slightly confsuing so here is a simple example of how to do it.

To get started

uv init
uv add pandas dash_ag_grid

Then run the code using

uv run main.py

main.py:

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

app = Dash()

rowData = [
    {
        "Name": "Iris-setosa",
        "Sepal": {"Width": 3.5, "Length": 5.1},
        "Petal": {"Width": 0.2, "Length": 1.4},
    },
    {
        "Name": "Iris-setosa",
        "Sepal": {"Width": 3.0, "Length": 4.9},
        "Petal": {"Width": 0.2, "Length": 1.4},
    },
]

columnDefs = [
    {"field": "Name"},
]

app.layout = html.Div(
    [
        html.Button("Update Row Data", id="update-row-data"),
        dag.AgGrid(
            id="column-definitions-nested-data",
            rowData=rowData,
            defaultColDef={"filter": True, "editable": True, "flex": 1},
            columnDefs=columnDefs,
            columnSize="responsiveSizeToFit",
            dashGridOptions={"animateRows": False},
        ),
    ]
)


@app.callback(
    [
        Output("column-definitions-nested-data", "rowData"),
        Output("column-definitions-nested-data", "columnDefs"),
        Output("column-definitions-nested-data", "columnSize"),
    ],
    [
        Input("update-row-data", "n_clicks"),
        Input("column-definitions-nested-data", "rowData"),
    ],
    prevent_initial_call=False,
)
def update_row_data(n_clicks, rowData):
    columnDefs = [
        {"field": "Name", "width": 200},
        {
            "field": "Sepal.Width",
            "valueSetter": {"function": "SetParams(params)"},
        },
        {
            "field": "Sepal.Length",
            "valueSetter": {"function": "SetParams(params)"},
        },
        {
            "field": "Area",
            "valueFormatter": {"function": "numberFormatter(params)"},
            "valueSetter": {
                "function": "SetParams(params)"
            },  # Called when I manually set the Area
        },
    ]

    df = pd.DataFrame.from_dict(rowData)
    # # df["Area"] = df["Sepal.Width"] * df["Sepal.Length"]
    # # df = df.explode(["Sepal", "Petal"])
    print(df)
    print(df.columns)
    # print(datetime.datetime.now())
    # rowData = df.to_dict(orient="records")
    return rowData, columnDefs, "responsiveSizeToFit"


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

assets/dashAgGridFunctions.js

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

dagfuncs.SetParams = (params) => {
    /**
     * Calculate the area based on the width and length fields.
     * @param {Object} params - The parameters containing data and column definitions.
     * @returns {boolean}
     * - Returns true if the area was successfully calculated and set, otherwise returns false.
     */

    // Start by setting the new value for the current column
    // Determine the active column
    const activeColumn = params.column.colId;
    const parts = activeColumn.split(".");
    if (parts.length > 1) {
        let obj = params.data;
        for (let i = 0; i < parts.length - 1; i++) {
            obj = obj[parts[i]];
        }
        obj[parts[parts.length - 1]] = params.newValue;
    } else {
        params.data[activeColumn] = params.newValue;
    }
    console.log("Setting params for column:", activeColumn, "with value:", params.newValue);
    if (activeColumn === "Area") {

        // params.data.Area = params.newValue;
        if (params.data.Sepal.Width != 0) {
            params.data.Sepal.Length = params.newValue / params.data.Sepal.Width; // Example of setting another field based on Area
        }
        else if (params.data.Sepal.Length != 0) {
            params.data.Sepal.Width = params.newValue / params.data.Sepal.Length;
        }
        else {
            // If both are null, we can't calculate anything
            params.data.Sepal.Width = Math.sqrt(params.data.Area);
            params.data.Sepal.Length = Math.sqrt(params.data.Area);
        }
    }
    // Then calculate derived values
    return dagfuncs.SetArea(params);
};

dagfuncs.SetArea = (params) => {
    /**
     * Calculate the area based on the width and length fields.
     * @param {Object} params - The parameters containing data and column definitions.
     * @returns {boolean}
     * - Returns true if the area was successfully calculated and set, otherwise returns false.
     */
    const width = params.data.Sepal.Width;
    const length = params.data.Sepal.Length;
    if (width != null && length != null) {
        params.data.Area = width * length;
        return true;
    }
    return false;
};

Now you can load the example and change Width, Length or Area freely. In the callback the value is also returned so you can use it in your Python code.