Implementing Advanced Features in ServerSide rowModel AG Grid

Hi, I’m currently creating an excel like web app that can visualize and analyze my huge csv data.
I almost done with all the core features using dash AG Grid

I used rowModelType that allows grouping, thanks to the help I received from the link below. I have modified the extractRowFromData() function to enable Filtering and sorting as well.
serverSide rowModelType in Dash AgGrid - Dash Python - Plotly Community Forum

However, I’m having difficulty implementing some additional features as bellow.
I not used to React of javascript, plus since Im using serverSide rowModel, Im struggling to do it by my self.
Please give me help if these features are possilbe.

  1. As shown in the blue box in the picture below, I want to display the counter number of data inside the group when I fold them using sidebar. If you run my code, row count is displayed in other columns(the red box below). And, I want the group to be sorted in order of high group count number when I click the Group column.

  2. When I fold by grouping, I want to display the first row data of hidden data in other columns to the red box in the picture below ( my current version of code shows the row group count number).

  1. I want to apply Advanced Filter to my dash ag table.

JavaScript Data Grid: Advanced Filter (ag-grid.com)

  1. I want to apply Statusbar(or Footer) to show the number of rows. show the number of currently visible rows in grid, so folded rows count as 1 row.

JavaScript Data Grid: Row Grouping - Group Footers (ag-grid.com)
JavaScript Data Grid: Status Bar (ag-grid.com)

I am using dash (version=2.14.1) and dash_ag_grid(version = 2.4.0)
Here is my current code that Im working on:

app.py

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

app = Dash(__name__)

server = app.server

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

rowData = df.to_dict("records")

columnDefs = [
    # Row group by country and by year is enabled.
    {"field": "country","sortable": True,"filter": True},
    {"field": "year", "sortable": True, "filter": True,},
    {"field": "athlete", "sortable": True, "filter": True},
    {"field": "age", "sortable": True, "filter": True},
    {"field": "date", "sortable": True, "filter": True},
    {"field": "sport", "sortable": True, "filter": True},
    {"field": "total", "sortable": True, "filter": True},
]




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 = []
    print(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

    if request["sortModel"]:
        sorting = []
        asc = []
        for sort in request["sortModel"]:
            sorting.append(sort["colId"])
            if sort["sort"] == "asc":
                asc.append(True)
            else:
                asc.append(False)
        dff = dff.sort_values(
            by=sorting, ascending=asc
        )

    groupBy = []
    if request["rowGroupCols"]:
        groupBy = [i["id"] for i in request["rowGroupCols"]]
    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()
                )
    dff = dff.sort_values(
        by=[i["colId"] for i in request["sortModel"]],
        ascending=[i["sort"] == "asc" for i in request["sortModel"]],
    )
    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=columnDefs,
            dashGridOptions={
                "rowSelection": "multiple",
                "sideBar": True,
                "rowGroupPanelShow": "always",
                "suppressRowGroupHidesColumns": True,
                # "groupIncludeFooter": True,
                # "groupIncludeTotalFooter": True,
                # "rowGroupPanelSuppressSort": True,
                # "enableAdvancedFilter": True,
            },
            defaultColDef=dict(
                resizable=True, enableRowGroup=True, enableValue=True, enablePivot=True
            ),
            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)

js.file:

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);
    setTimeout(function () {
      params.success(result);
    }, 200);
  },
};
return dataSource;
}

I found a way to do my problem #1: Child Counts in serverSide rowModel.

From the link below, I add getChildCount to dashGridOptions to manipulate getChildCount(dataItem).

\docs\examples\columns\assets\dashAgGridFunctions.js

Check the code bellow, and hope it helps someone else.

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
from log import logger

app = Dash(__name__)

server = app.server

df = pd.read_csv(
    "https://raw.githubusercontent.com/plotly/datasets/master/ag-grid/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 = []

    logger.info(f"{pprint.pformat(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:
        # 그룹화된 행이 정렬 기준인 경우, childCount 기준으로 정렬
        logger.info(f"sorting: {sorting}")
        logger.info(f"asc: {asc}")
        logger.info(f"groupBy[0]: {groupBy[0]}")

        if groupBy[0] in sorting:
            logger.info(f"dff: {dff.head()}")
            logger.info(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)

    logger.info(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)
    # logger.info(pprint.pformat(response))
    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},  # 기본적으로 숨김
            ],
            defaultColDef={
                "filter": True,
                "sortable": True,
                "resizable": True,
                "enableRowGroup": True,
                "enableValue": True,
                "enablePivot": 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\dashAgGridFunctions.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;
}
3 Likes