Retrieving Information for All Modified Cells in dash_ag_grid

Hi,
I’m currently using dash_ag_grid to construct a dash webapp with features similar to Excel.
I’m dealing with big CSV data, hence I’m using ‘infinite’ rowModel (extually its ‘serverside’).
I’ve activated the EnterpriseModules and using gridOptions like below

dashGridOptions={
    "undoRedoCellEditing": True,
    "enableRangeSelection": True,
    "undoRedoCellEditing": True,
}

I’ve made it editable and when a cell is modified, I’ve synchronized it to modify the corresponding cell in the dataframe through ‘cellValueChanged’ in the callback.
(referenced from Editing and Callbacks | Dash for Python Documentation | Plotly)

The problem arises when I select a range of cells, copy and then paste it elsewhere, as in Excel. Even though multiple cells get modified at once, in the callback, ‘cellValueChanged’ shows information for only one cell (the last one among the modified cells).

Since the rowModel is ‘infinite’, if the modifications are not reflected in the dataframe, the changes will all be lost when the grid is refreshed.

How can I retrieve information for all the modified cells?

I’m sharing the code I’m using for your reference.

import dash_ag_grid as dag
from dash import Dash, Input, Output, html, no_update, callback, State, Patch
import pandas as pd
import dash_bootstrap_components as dbc
app = Dash(__name__)


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

columnDefs = [
    {
        "headerName": "ID",
        "maxWidth": 100,
        "valueGetter": {"function": "params.node.id"},
    },
    {"field": "athlete", "minWidth": 150},
    {"field": "country", "minWidth": 150},
    {"field": "year"},
    {"field": "sport", "minWidth": 150},
    {"field": "total"},
]

defaultColDef = {
    "flex": 1,
    "minWidth": 150,
    "sortable": False,
    "resizable": True,
    'editable': True
}


adding_newColumn = dbc.Col(
    [
        html.Div("New Column:"),
        dbc.InputGroup(
            [
                dbc.Input(
                    id="new-column-header-input",
                    placeholder="header",
                    type="text",
                    size="sm",
                    className="smaller-input",
                ),
                dbc.Input(
                    id="new-column-value-input",
                    placeholder="value",
                    type="text",
                    size="sm",
                    className="smaller-input",
                ),
                dbc.Button(
                    "Add",
                    id='add-new-column-btn',
                    color="secondary",
                    size="sm",
                    className="me-1",
                    # disabled=True,
                ),
            ],
            size="sm",
        ),
    ],
    width=2,
)

app.layout = html.Div(
    [
        adding_newColumn,
        dag.AgGrid(
            id="infinite-row-grid",
            columnDefs=columnDefs,
            defaultColDef=defaultColDef,
            rowModelType="infinite",
            enableEnterpriseModules=True,
            dashGridOptions={
                "undoRedoCellEditing": True,
                "enableRangeSelection": True,
                "undoRedoCellEditing": True,
            },

            style={"height": "70vh"},
        ),
        html.Div(id="editing-grid-output"),
    ],
)


@callback(
    Output("infinite-row-grid", "getRowsResponse"),
    Input("infinite-row-grid", "getRowsRequest"),
    Input("infinite-row-grid", "columnDefs"),
)
def infinite_scroll(request, columnDefs):
    if request is None:
        return no_update
    partial = df.iloc[request["startRow"]: request["endRow"]]
    return {"rowData": partial.to_dict("records"), "rowCount": len(df.index)}


@callback(
    Output("editing-grid-output", "children"), Input("infinite-row-grid", "cellValueChanged")
)
def update(cell_changed):
    return f"{cell_changed}"


@callback(
    Output("infinite-row-grid", "columnDefs"),
    Input("add-new-column-btn", "n_clicks"),
    State("new-column-header-input", "value"),
    State("new-column-value-input", "value"),
    prevent_initial_call=True,
)
def add_column(n, header, value):
    global df
    df[header] = value
    patched_columnDefs = Patch()
    patched_columnDefs.append({"field": header, 'editable': True})
    print(df.head())

    return patched_columnDefs


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

Thankyou

Hello @wowwwn,

What happens in the console if you add this event listener? Do you see all the events?

dash_ag_grid.getApiAsync(id).then((grid) => grid.addEventListener(‘cellValueChanged’, (e) => console.log(e)))

Put this in a clientside_callback on the grid’s id and then return window.dash_clientside.no_update

2 Likes

Hello @jinnyzor,

I am always grateful for your prompt responses.

I am not very familiar with JavaScript, but I tried adding the following code as you suggested:

app.clientside_callback(
    """async function (id) {
        
        dash_ag_grid.getApiAsync(id).then((grid) => grid.addEventListener('cellValueChanged', (e) => console.log(e)))
        
        return window.dash_clientside.no_update
    }""",
    Output("infinite-row-grid", "id"),
    Input("infinite-row-grid", "id"),
)

When I pressed ‘F12’ to look at the console in the chrome browser, I confirmed that it outputs the information I wanted (cellValueChanged for all modified cells).

As I’m familiar with Python and less so with JavaScript or React, I wonder if there’s a way to handle this information in a Python callback?

Also, I’m very interested in the advanced features of ag_grid that are not supported in dash_ag_grid. Do you have any recommended resources for JavaScript or React I could refer to?

I truly appreciate your help every time. I’m not sure how I can ever repay your kindness.

Merry Christmas

Do you get all events, if you use a clientside callback that just prints to console?

Yes, but only last one in the callback

Ok.

So then you’ll need to make your own handle for the events, there is probably something that keeps it from triggering too quickly.

You can always check your network tab to see if the callbacks are being sent to the server.

Infinite scrolling should have a cache though, so as you scroll back and forth and up and down you should be pulling from the cache. The issue is syncing the cellValueChanged to the server.

What you could do is build your own store on the clientside that can collect the data and then pass it in chunks periodically.

The thing I am wondering, is this info shared among other users? If they need these changes realtime, then you might need to use something that can send the fetch requests at a much higher rate and the server that can handle a higher frequency of updates.

If these are only local to your user, then I think there isn’t an issue with just allowing the grid to cache the data and then when you leave, have it push the store of changes back to the df.

As an aside, typically I shy away from realtime changes and push changes on a button push by the user. You can run into issues with a db being locked or people stepping on each others toes, etc.

1 Like

What about attaching a custom event handler for the cell changed event that collects all events with some delay (say, 100 ms or so), and then fires a new event to trigger a Dash callback with the resulting list of events, which event are no longer arriving?

EDIT: Maybe this could actually be implemented as part of dash agreed itself (:slight_smile:

1 Like

Hmm. That might work.

Would change this to a list though. We could potentially do it in the open PR to the next version.

1 Like

We could change type to union of list/single element, set default delay to -1, in which case a single element is returned, thus preserving backwards compatibility.

But, this could be confusing as far as writing codes where a single element is still returned when multiple is expected.

The lift for cellValueChanged would be a find and replace with adding on a [0], where the alternative would be to check isinstance list or dict.

That’s true. So I guess the question is if we value backwards compatibility versus returning the same type (list) always. I can see the benefits of each choice - but I tend to lean towards backwards compatibility for an enterprise component like ag-grid😊

There’s already breaking changes in the next version, so one more is ok. Plus it makes sense to not have to deal with it again. Haha.

At least that’s my thinking. :grin:

Thank you. I was able to solve it with the hint you gave me.

I used the fact that in the clientside_callback, we can check the information we want in the cellValueChanged EventListener. In the clientside_callback, I implemented a CellValueChange in Javascript and passed the necessary information to the server side for processing using the server function of Flask’s server by posting it to the CellValueChange function, and stored all the cell information that needs to be updated in a global set variable called updated_cell_cache.

  • app.py:

    @server.route("/api/CellValueChange", methods=["POST"])
    def CellValueChange():
    	data = flask.request.json
    	col_id = data['colId']
    	new_value = data['value']
    	unique_id = data['unique_id']
    	global updated_cell_cache
    	updated_cell_cache.add((unique_id, col_id, new_value))
    	return "ok"
    
    
    
    app.clientside_callback(
    	"""async function (id) {
    		const delay = ms => new Promise(res => setTimeout(res, ms));
    		const updateData = (grid) => {
    			var datasource = createServerSideDatasource();
    			grid.setServerSideDatasource(datasource);
    		};
    
    		const onCellValueChanged = async (event) => {
    			if (event.type === 'cellValueChanged'){
    				try {
    					await CellValueChange(event);
    				} catch (error) {
    					console.error("Error sending cell value change:", error)
    				}
    				return window.dash_clientside.no_update
    			}
    		};
    
    		const getGrid = async (id) => {
    			let grid;
    			let count = 0;
    			while (!grid) {
    				await delay(200);
    				try {
    					grid = dash_ag_grid.getApi(id);
    				} catch {}
    				count++;
    				if (count > 20) {
    					break;
    				}
    			}
    			return grid;
    		};
    		
    		dash_ag_grid.getApiAsync(id).then((grid) => grid.addEventListener('cellValueChanged', onCellValueChanged));
    
    		const grid = await getGrid(id);
    		if (grid) {
    			await updateData(grid);
    		}
    	
    		return window.dash_clientside.no_update
    	}""",
    	Output({"type": "aggrid-table", "index": MATCH}, "id"),
    	Input({"type": "aggrid-table", "index": MATCH}, "id"),
    )
    
    
  • assets/dag.js

    async function CellValueChange(event) {
        try{
            const response = await fetch('./api/CellValueChange', {
                method: 'POST',
                body: JSON.stringify({colId: event['column']['colId'], value: event['value'], unique_id: event['data']['unique_id']}),
                headers: { 'content-type': 'application/json' }
            });
            if (!response.ok) {
                throw new Error("Network response was not ok.")
            }
        } catch (error){
            console.error("Failed to send cell value changed:", error);
        }
    }
    

Then, in the dash_callback called cellrange_edit_update, I was able to fully update my dataframe by retrieving all the updated cell information from the updated_cell_cache.

@app.callback(
	Output({"type": "editable-grid", "index": MATCH}, "value"),
	Input({"type": "aggrid-table", "index": MATCH},
		  "cellValueChanged"),
	State({"type": "aggrid-table", "index": MATCH}, "id"),
	prevent_initial_call=True,
)
def cellrange_edit_update(cell_changed, idx):
	while updated_cell_cache:
		unique_id, col_id, dfId, new_value = updated_cell_cache.pop()

		global df
		df.loc[df['unique_id'] == unique_id, col_id] = new_value
		
	raise exceptions.PreventUpdate()

Do you know when the next version of dash_ag_grid will be released?
I’m looking forward to seeing many important features, such as advanced filters, added in the update.

Good job, that’s what I was thinking of. :grin:

And as far as when the next version, depends on documentation.

@wowwwn

Be sure to check this section - how updating global variables in a callback can break your app:

2 Likes

Thank you for advice. The link you provided was very helpful.
(I am also enjoying “The Book of Dash”)

Since there are many things to consider for multiuser,
considering my situation, I’ll make users to run webserver in there local and use it for there own with 1 worker.
So, what I will do after finish developing my dash app, I will make it to exe binary file(like QT gui app, Im not sure if it possible though…)

I have created a PR to enable the requested functionality. If everything goes well, I hope it will be part of the next release :slight_smile:

3 Likes