Implementing Row Groupings Modification Feature in Dash Ag Grid

Hi,
I am currently using the dash ag grid to create a dash app and finding it very useful. However, I am unsure how to implement the following feature and would appreciate any help.

I would like to implement a feature where if a certain column in a group row is modified when Row Groupings are done, all the column data within that group is modified.

For example, if you run my provided dash app below, all data cells can be edited, but the row representing the group is blocked to edit.


I would like to modify the cell in the red box to change all the column data in that group to the same value.

I am sharing my current version of the dash app code (app.py, assets/dashAgGrid.js). I am using the rowModel as serverSide.

Thank you always.

#app.py 

from dash import Dash, Input, Output, html, dcc, State
import dash_ag_grid as dag
import requests, json
import flask
import pandas as pd
import pprint

app = Dash(__name__)

server = app.server

df = pd.read_csv(
    #"https://raw.githubusercontent.com/plotly/datasets/master/ag-grid/olympic-winners.csv"
    "./datasets/olympic-winners.csv"
)
df = df.dropna()
rowData = df.to_dict("records")


def filterDf(df, data, col):
    operators = {
        "greaterThanOrEqual": "ge",
        "lessThanOrEqual": "le",
        "lessThan": "lt",
        "greaterThan": "gt",
        "notEqual": "ne",
        "equals": "eq",
    }

    if "filter" in data:
        crit1 = data["filter"]
        crit1 = pd.Series(crit1).astype(df[col].dtype)[0]

    if "type" in data:
        if data["type"] == "contains":
            df = df.loc[df[col].str.contains(crit1, na=False)]
        elif data["type"] == "notContains":
            df = df.loc[~df[col].str.contains(crit1, na=False)]
        elif data["type"] == "startsWith":
            df = df.loc[df[col].str.startswith(crit1, na=False)]
        elif data["type"] == "notStartsWith":
            df = df.loc[~df[col].str.startswith(crit1, na=False)]
        elif data["type"] == "endsWith":
            df = df.loc[df[col].str.endswith(crit1, na=False)]
        elif data["type"] == "notEndsWith":
            df = df.loc[~df[col].str.endswith(crit1, na=False)]
        elif data["type"] == "blank":
            df = df.loc[df[col].isnull()]
        elif data["type"] == "notBlank":
            df = df.loc[~df[col].isnull()]
        else:
            df = df.loc[getattr(df[col], operators[data["type"]])(crit1)]
    elif data["filterType"] == "set":
        df = df.loc[df[col].isin(data["values"])]
    return df


def extractRowsFromData(request, df):
    response = []

    pprint.pprint(request)

    dff = df.copy()

    if request["filterModel"]:
        fils = request["filterModel"]
        for k in fils:
            try:
                if "operator" in fils[k]:
                    if fils[k]["operator"] == "AND":
                        for fil in fils[k]["conditions"]:
                            dff = filterDf(dff, fil, k)
                    else:
                        dffs = []
                        for fil in fils[k]["conditions"]:
                            dffs.append(filterDf(dff, fil, k))
                        dff = pd.concat(dffs)
                else:
                    dff = filterDf(dff, fils[k], k)
            except:
                pass

    sorting = []
    asc = []
    if request["sortModel"]:
        for sort in request["sortModel"]:
            sorting.append(sort["colId"])
            asc.append(sort["sort"] == "asc")

        # dff = dff.sort_values(by=sorting, ascending=asc)

    groupBy = []
    if request["rowGroupCols"]:
        groupBy = [i["id"] for i in request["rowGroupCols"]]

    if groupBy:
        group_counts = dff.groupby(groupBy).size().reset_index(name="childCount")
        dff = dff.merge(group_counts, on=groupBy, how="left")

    agg = {}
    if request["valueCols"]:
        agg = {i["id"]: i["aggFunc"] for i in request["valueCols"]}
    if not request["groupKeys"]:
        if groupBy:
            if agg:
                dff = dff.groupby(groupBy[0]).agg(agg).reset_index()
            else:
                dff = dff.groupby(groupBy[0]).agg("count").reset_index()
    else:
        for i in range(len(request["groupKeys"])):
            dff = dff[dff[request["rowGroupCols"][i]["id"]] == request["groupKeys"][i]]
        if len(request["groupKeys"]) != len(groupBy):
            if agg:
                dff = (
                    dff.groupby(groupBy[: len(request["groupKeys"]) + 1])
                    .agg(agg)
                    .reset_index()
                )
            else:
                dff = (
                    dff.groupby(groupBy[: len(request["groupKeys"]) + 1])
                    .agg("count")
                    .reset_index()
                )

    if groupBy:
        print(f"sorting: {sorting}")
        print(f"asc: {asc}")
        print(f"groupBy[0]: {groupBy[0]}")

        if groupBy[0] in sorting:
            print(f"dff: {dff.head()}")
            print(f"asc[0]: {asc[0]}")

            dff = dff.sort_values(by="childCount", ascending=asc[0])
        else:
            dff = dff.sort_values(by=sorting, ascending=asc)
    else:
        dff = dff.sort_values(by=sorting, ascending=asc)

    print(f"{pprint.pformat(dff)}")
    return {
        "rowData": dff.to_dict("records")[request["startRow"] : request["endRow"]],
        "rowCount": len(dff),
    }


@server.route("/api/serverData", methods=["POST"])
def serverData():
    response = extractRowsFromData(flask.request.json, df)
    return json.dumps(response)


grid = html.Div(
    [
        dag.AgGrid(
            id="grid",
            columnDefs=[
                {"field": "country"},
                {"field": "year"},
                {"field": "athlete"},
                {"field": "age"},
                {"field": "date"},
                {"field": "sport"},
                {"field": "total"},
                # {"field": "childCount", "hide": True},  # hide default
            ],
            defaultColDef={
                "filter": True,
                "sortable": True,
                "resizable": True,
                "enableRowGroup": True,
                "enableValue": True,
                "enablePivot": True,
                'editable': True,
            },
            dashGridOptions={
                "rowSelection": "multiple",
                "sideBar": True,
                "rowGroupPanelShow": "always",
                "suppressRowGroupHidesColumns": True,
                "getChildCount": {"function": "getChildCount(params)"},
            },
            enableEnterpriseModules=True,
            rowModelType="serverSide",
            style={"overflow": "auto", "resize": "both", "height": "60vh"},
        ),
    ]
)

app.layout = html.Div(
    [
        dcc.Markdown("Example: Organisational Hierarchy using Tree Data "),
        grid,
    ]
)

app.clientside_callback(
    """async function (id) {
        
        const updateData = (grid) => {
          var datasource = createServerSideDatasource();
          grid.setServerSideDatasource(datasource);
        };
        
        var grid;
        grid = await window.dash_ag_grid.getApiAsync(id)
        
        if (grid) {
            updateData(grid)
        }
        
        return window.dash_clientside.no_update
    }""",
    Output("grid", "id"),
    Input("grid", "id"),
)

if __name__ == "__main__":
    app.run(debug=True)
# assets/dashAgGrid.js
async function getServerData(request) {
    response = await fetch('./api/serverData', {'method': 'POST', 'body': JSON.stringify(request),
      'headers': {'content-type': 'application/json'}})
    return response.json()
  }
  
  
  function createServerSideDatasource() {
    const dataSource = {
      getRows: async (params) => {
        console.log('ServerSideDatasource.getRows: params = ', params);
        var result = await getServerData(params.request);
        console.log('getRows: result = ', result);
        params.success(result);
      },
    };
    return dataSource;
  }
  
  
  
  var dagfuncs = window.dashAgGridFunctions = window.dashAgGridFunctions || {};
  
  dagfuncs.getChildCount =  function(data) {
    // here child count is stored in the 'childCount' property
    // console.log('data = ', data);
    return data.childCount;
  }

I’ve found out that adding “enableGroupEdit”: True to dashGridOptions makes the group row editable.

Now I have to implement the feature to the callback function below using the ‘cellValueChange’ of the grid component after editing.

However, I don’t know how to call the current grid’s grouping information.

@app.callback(
    Output("grid", "cellValueChanged"),
    Input("grid", "cellValueChanged"),
    prevent_initial_call=True,
)
def update(cell_changed):

    print("cell_changed")
    print(cell_changed)

    raise exceptions.PreventUpdate

Hello @wowwwn,

So, two things here:

1 - be sure you have a license from AG grid in order to use their enterprise features, $700 I think a year for internal use per dev.
2 - use version v31, because the cellValueChanged becomes an array of the changes vs the last cell change.

Here is a link to AG Grid Enterprise pricing information:

I am currently developing a side project on my own, so purchasing the enterprise version is a bit burdensome. And Im not sure if I will keep using AG Grid since my workplace uses C++ QT. Once the review is complete and if it’s deemed necessary to develop using AGGrid, I will request my workplace to make the necessary purchase.

Thanks for the reply.