How to correctly data type & input in Dash AG GRID? and other issues

Hello guys,

I have switched from Dash DataTable to DASH AG GRID after this post:

QUESTION ABOUT FORMATTING SOME VALUES IN A DASH DATATABLE (WHEN USING A TRANSPOSED TABLE)

In the question above, I focused on a specific problem, but after switching my table to AG GRID I started to find new limitations as I have text, boolean, and others data types and I can’t find solutions to deal with it.

Below are some of the new issues/limitations I found:

  • Sometimes, the user does not add/change anything but because of as the DASH AG GRID recognize the data types the changes are triggered;

  • I can’t highlight a Specific cell based on which one was changed

  • I can’t find a way to force the cells to render the entire values inside the cells without the …

image
Iin dash datatable we could deal with this kind of problem by simple using:

style_data={"whiteSpace": "normal",
            "overflow": "hidden",
            "textOverflow": "ellipsis"}

__________________________________


Below I will add a Minimal reproducible example

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

list_of_vals = [
    {
        "Val ID": "9729",
        "Year Col": "2017",
        "Hours Col": 1253.0,
        "Value Col": 31950000.0,
        "Text Col": "testing a random text which is long enough to wrap",
        "Bool Col": "No",
        "Date Col": "2023/01/02",
        "Link Col": "https://www.google.com",
    },
    {
        "Val ID": "9733",
        "Year Col": "2016",
        "Hours Col": 2541.0,
        "Value Col": 29995000.0,
        "Text Col": "testing a random text which is long enough to wrap",
        "Bool Col": "Yes",
        "Date Col": "2023/01/02",
        "Link Col": "https://www.google.com",
    },
    {
        "Val ID": "9837",
        "Year Col": "2019",
        "Hours Col": 566.0,
        "Value Col": 34900000.0,
        "Text Col": "testing a random text which is long enough to wrap",
        "Bool Col": "No",
        "Date Col": "2023/01/02",
        "Link Col": "https://www.google.com",
    },
    {
        "Val ID": "9507",
        "Year Col": "2013",
        "Hours Col": 2732.0,
        "Value Col": 26000000.0,
        "Text Col": "testing a random text which is long enough to wrap",
        "Bool Col": "Yes",
        "Date Col": "2023/01/02",
        "Link Col": "https://www.google.com",
    },
    {
        "Val ID": "9805",
        "Year Col": "2018",
        "Hours Col": 1543.0,
        "Value Col": 31500000.0,
        "Text Col": "testing a random text which is long enough to wrap",
        "Bool Col": "Yes",
        "Date Col": "2023/01/02",
        "Link Col": "https://www.google.com",
    },
]

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()

app.layout = html.Div(
    [
        dcc.Store(id="changed-cell", data=[]),
        html.H2("Testing Dash AG GRID"),
        dag.AgGrid(
            rowData=data_list,
            columnDefs=[{"field": "Val ID", "editable": False}]
            + [
                {
                    "field": c,
                    "valueFormatter": {"function": "FormatNumbersByRow(params)"},
                    # "editable": {"function": "params.data['Val ID'] !== 'Year Col'"}
                }
                for c in columns_list
                if c != "Val ID"
            ],
            defaultColDef={"editable": True, "flex": 1},
            dashGridOptions={
                "undoRedoCellEditing": False,
                "undoRedoCellEditingLimit": 20,
            },
            id="ag-grid-table",
        ),
        dcc.Store(id="original-data", data=data_list),
        html.Div(id="edited-table-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:
                print(row)
                old_value = row[key]
            else:
                pass

        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):
    print(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",
                    ),
                ]
            )


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

and the content of assets/agGridFunctions.js (I’ve added the console.log to help to validate it, but it only works for the numerical values =/ )

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"] == "Text Col") {
        console.log(params.value);
        return params.value.toString();
    }
    if (params.data["Val ID"] == "Hours Col") {
        console.log(params.value);
        return parseInt(params.value);
    }
    if (params.data["Val ID"] == "Bool Col") {
        console.log(params.value);
        return params.value.toLowerCase() === 'yes';
    }
    if (params.data["Val ID"] == "Value Col") {
        return Intl.NumberFormat("en-US", {
            style: "currency",
            currency: "USD",
            minimumFractionDigits: 0,
            maximumFractionDigits: 0,
          }).format(params.value);
    }
}

If you have any info about how to proceed to solve any of the issues I mentioned above, I would love to know it; Any help is appreciated

Hi @kabure

You certainly picked a very advanced use-case for your first dash-ag-grid. :sweat_smile:
Just so you know, this would be much easier with AG Grid Enterprise - it has a built-in pivot mode:

If you don’t want to buy a license, it’s still possible to do this with the community version, it will just take a little more coding.

For the truncated values, you have lots of options.

For different data types by row, set the cellDataType to False so the inference is turned off. For more info see Cell Data Types | Dash for Python Documentation | Plotly.

You can set different cell editors per line. See Provided Cell Editors | Dash for Python Documentation | Plotly. and also React Data Grid: Cell Editors

               {
                    "field": c,
                    "valueFormatter": {"function": "FormatNumbersByRow(params)"},
                    "cellEditorSelector": {"function": "cellEditorSelector(params)"},        
                    'cellDataType': False
                }



dagfuncs.cellEditorSelector = function(params) {
    if (params.data["Val ID"] == "Text Col") {
        return {
            component: "agTextCellEditor",
        }
    }
    if (["Hours Col", "Values Col"].includes(params.data["Val ID"])) {
        return {
            component: "agNumberCellEditor",
        }
    }
    if (params.data["Val ID"] == "Bool Col") {
        return {
          component: 'agSelectCellEditor',
          params: {
            values: ['Yes', 'No'],
          },
          popup: true,
        };
    }
    return undefined;
}


You could have actual boolean values instead of the select editor, and it could render as a checkbox. If you can change the date format to yyyy-mm-dd you could also easily add a date editor component.

Here’s a post on how to highlight edited values Dash AG Grid single cell styling after cell edit - #2 by jinnyzor

3 Likes

Hello @AnnMarieW

Thank you so much, AGAIN, for all the info, dear!

EDIT: I have implemented the height & Data type solutions and both them worked like a charm :raised_hands:
The highlight thing also worked, but it has some issues that I have asked for jinnyzor and Cantelli in the thread you mentioned

I have two extra questions that I would like to ask you if you have some time to take a look (if you prefer, I can create a new topic)
1 - One of the fields is a https link… There’s a way to transform it in a hyperlink? I tried to implement a solution I found in the docs but seems it only works for columns
image

2 - There’s a way to avoid that the inputs be empty values? I’m asking this because I did note that if I have a numerical cell and I try to input a different data type as text, it highlights the cell in red and the cell becomes empty/None instead of rejecting the new input, and if I press the key E it will raise an error (as you can see in the .gif)
CPT2404251529-370x342
I think that the solution here would be to keep to “oldValue” when the “newValue” is null/None

EDIT: PLEASE, DON’T WORRY ABOUT THE SECOND ISSUE! I HAVE FOUND A SOLUTION TO IT

Do you have any experience with these this new two issues?

PS: I have added a new Link Colin the MRE in the main thread message

Again, thank you for all your help, and thank you in advance for the new help

Hi @kabure

Glad you found a solution to the second issue :slight_smile:

For the link you could use a cell renderer. The easiest solution might be using the built-in Markdown component. If you want to create your own custom component, note that it needs to be defined in a different namespace window.dashAgGridComponentFunctions

Learn more about cell renderers and custom components here:

There is an example of different cell renderers per row in the docs.

Hi @AnnMarieW

Thank you so much for the references!

After some tests and headache, I just found the correct way to use a custom component to deal with the link:
The change in the agGrid declaration:


The custom component:
image


I feel like the biggest part of the challenges I'm facing is caused because of the "columns" are rows :hot_face:

Now it only remains a few smaller things;

I have learned a lot about the potential of AG-GRID building this table;

Thank you very much for all the support and references!

Hi @kabure

Yes - that looks correct to me!

And true, all this would be much, much easier if the data wasn’t transposed. But it’s a good way to demonstrate that the grid is highly customizable :slight_smile:

1 Like