Hello guys,
I have some dynamic cells in my DASH AG GRID, which I would like to update some fields based on others;
It is working as expected but one issue I’m facing is that once it sends the updated data it scrolls up the table to the top
I would like to update the data fields but keep the table in that specific field without reloading it and scrolling up
Below is an MRE:
import dash
from dash import html, Dash, Output, Input, State, dcc
import dash_ag_grid as dag
import pandas as pd
import dash_mantine_components as dmc
list_of_vals = [
{
"Val ID": "Item 001",
"Year Col": "2017",
"Text Col": "testing a random text which is long enough to wrap",
"Bool Col": "No",
"Options Col": "OTHER OPTION 2",
"Long List Col": "option 1",
"Country Col": "Brazil",
"Date Col": "2024-01-01",
"Hours Col": 1253.0,
"Value Col": None,
},
{
"Val ID": "Item 002",
"Year Col": "2016",
"Text Col": "testing a random text which is long enough to wrap",
"Bool Col": "Yes",
"Options Col": "OTHER OPTION 3",
"Long List Col": "option 2",
"Country Col": "France",
"Date Col": "2024-01-01",
"Hours Col": 2541.0,
"Value Col": None
},
{
"Val ID": "Item 003",
"Year Col": "2019",
"Text Col": "testing a random text which is long enough to wrap",
"Bool Col": "Yes",
"Options Col": "OTHER OPTION 3",
"Long List Col": "option 5",
"Country Col": "China",
"Date Col": "2024-01-01",
"Hours Col": 566.0,
"Value Col": None
},
{
"Val ID": "Item 004",
"Year Col": "2013",
"Text Col": "testing a random text which is long enough to wrap",
"Bool Col": "No",
"Options Col": "OTHER OPTION 1",
"Long List Col": "option 12",
"Country Col": "China",
"Date Col": "2024-01-01",
"Hours Col": 2732.0,
"Value Col": None
},
{
"Val ID": "Item 005",
"Year Col": "2018",
"Text Col": "testing a random text which is long enough to wrap",
"Bool Col": "No",
"Options Col": "OTHER OPTION 2",
"Long List Col": "option 8",
"Country Col": "France",
"Date Col": "2024-01-01",
"Hours Col": 1543.0,
"Value Col": None
},
{
"Val ID": "Item 006",
"Year Col": "2011",
"Text Col": "testing a random text which is long enough to wrap",
"Bool Col": "Yes",
"Options Col": "OTHER OPTION 6",
"Long List Col": "option 7",
"Country Col": "Brazil",
"Date Col": "2024-01-01",
"Hours Col": 1543.0,
"Value Col": None
},
{
"Val ID": "Item 007",
"Year Col": "2012",
"Text Col": "testing a random text which is long enough to wrap",
"Bool Col": "Yes",
"Options Col": "OTHER OPTION 4",
"Long List Col": "option 10",
"Country Col": "Italy",
"Date Col": "2024-01-01",
"Hours Col": 1543.0,
"Value Col": None
},
{
"Val ID": "Item 008",
"Year Col": "2017",
"Text Col": "testing a random text which is long enough to wrap",
"Bool Col": "Yes",
"Options Col": "OTHER OPTION 6",
"Long List Col": "option 9",
"Country Col": "France",
"Date Col": "2024-01-01",
"Hours Col": 1543.0,
"Value Col": None
},
{
"Val ID": "Item 009",
"Year Col": "2016",
"Text Col": "testing a random text which is long enough to wrap",
"Bool Col": "Yes",
"Options Col": "OTHER OPTION 4",
"Long List Col": "option 14",
"Country Col": "China",
"Date Col": "2024-01-01",
"Hours Col": 1543.0,
"Value Col": None
},
{
"Val ID": "Item 010",
"Year Col": "2016",
"Text Col": "testing a random text which is long enough to wrap",
"Bool Col": "No",
"Options Col": "OTHER OPTION 3",
"Long List Col": "option 14",
"Country Col": "Germany",
"Date Col": "2024-01-01",
"Hours Col": 1543.0,
"Value Col": None
},
{
"Val ID": "Item 011",
"Year Col": "2012",
"Text Col": "testing a random text which is long enough to wrap",
"Bool Col": "No",
"Options Col": "OTHER OPTION 9",
"Long List Col": "option 8",
"Country Col": "France",
"Date Col": "2024-01-01",
"Hours Col": 1543.0,
"Value Col": None
},
{
"Val ID": "Item 012",
"Year Col": "2020",
"Text Col": "testing a random text which is long enough to wrap",
"Bool Col": "Yes",
"Options Col": "OTHER OPTION 7",
"Long List Col": "option 35",
"Country Col": "Germany",
"Date Col": "2024-01-01",
"Hours Col": 1543.0,
"Value Col": None
},
]
for val in list_of_vals:
val["Value Col"] = val["Hours Col"]*30
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()
def get_country_values():
return {"long_list_options": [f"option {n}" for n in range(2000)], "country":[
"Afghanistan",
"Aland Islands",
"Albania",
"Algeria",
"France",
"Brazil",
"China",
"Germany",
"Italy",
"Christmas Island",
"Cocos (Keeling) Islands",
"Tahiti",
"TBD",
]}
app.layout = html.Div(
[
dcc.Store(id="changed-cell", data=[]),
html.H2("Dash AG GRID - Rows cell styling"),
dag.AgGrid(
rowData=data_list,
columnDefs=[{"field": "Val ID", "editable": False}]
+ [
{
"field": c,
"cellRenderer": "FormatSpecificCells",
"valueFormatter": {"function": "FormatNumbersByRow(params)"},
"cellEditorSelector": {
"function": f"cellEditorSelector(params)"
},
"cellDataType": False,
"minWidth": 80,
"wrapText": True,
"cellEditor": {"function": "DMC_Select"},
"cellEditorParams": {
"options": get_country_values(),
"clearable": True,
"placeholder": "Select Option",
},
"autoHeight": True,
"valueSetter": {"function": "testValue(params)"},
"editable": {
"function": "['Value Col'].indexOf(params.data['Val ID']) === -1"
},
}
for c in columns_list
if c != "Val ID"
],
defaultColDef={"editable": True},
dashGridOptions={
"undoRedoCellEditing": False,
"undoRedoCellEditingLimit": 20,
},
id="ag-grid-table",
style={"height": "300px"},
),
dcc.Store(id="original-data", data=data_list),
html.Div(id="edited-table-output"),
html.Div(id="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:
old_value = row[key]
else:
pass
# Test if the old and new values are the same
if new_value == old_value:
changes_stored = [
changes_stored
for changes_stored in changes_stored
if changes_stored["change_id"] != change_id
]
return changes_stored
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):
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",
),
]
)
@app.callback(
Output("ag-grid-table", "columnDefs"),
Input("changed-cell", "data"),
State("ag-grid-table", "columnDefs"),
)
def update_content_output(changed_cells, columns):
key_to_check = "cellStyle"
if changed_cells == []:
to_change_cells = 0
for (n, val) in enumerate(columns):
if key_to_check in val.keys():
if val["cellStyle"] == {}:
pass
else:
to_change_cells += 1
val["cellStyle"] = {}
else:
pass
if to_change_cells == 0:
return dash.no_update
else:
return columns
list_of_sn = [val["val-ID"] for val in changed_cells]
for (n, val) in enumerate(columns):
if val["field"] in list_of_sn:
sn_val = val["field"]
list_of_rows = [
int(get_rows["index_row"])
for get_rows in changed_cells
if get_rows["val-ID"] == sn_val
]
cellStyle = {
"styleConditions": [
{
"condition": f"[{','.join(map(str, list_of_rows))}].includes(params.node.childIndex)",
"style": {"backgroundColor": "yellow"},
},
{
"condition": f"![{','.join(map(str, list_of_rows))}].includes(params.node.childIndex)",
"style": {},
},
]
}
val["cellStyle"] = cellStyle
else:
if key_to_check in val.keys():
if val["cellStyle"] != {}:
val["cellStyle"] = {}
# else:
# pass
else:
pass
else:
pass
return columns
@app.callback(
Output("ag-grid-table", "rowData"),
Input("ag-grid-table", "cellValueChanged"),
State("ag-grid-table", "rowData"),
)
def update_rowdata(change, full_data):
if change is None:
return dash.no_update
column=change[0]['data']['Val ID']
change_val_id=change[0]['colId']
if column != "Hours Col":
return dash.no_update
new_value_hours=change[0]['value']
row_index={val["Val ID"]:n for (n, val) in enumerate(full_data)}
value_col_index=row_index["Value Col"]
full_data[value_col_index][change_val_id]=new_value_hours*30
return full_data
if __name__ == "__main__":
app.run_server(debug=True, port=8050)
on your assets
folder you need to add agGridFunctions.js
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"] == "Hours Col") {
return Intl.NumberFormat("en-US").format(params.value);
}
if (params.data["Val ID"] == "Value Col") {
return Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(params.value);
}
}
dagfuncs.cellEditorSelector = function(params, list_vals) {
if (["Text Col", "Year Col"].includes(params.data["Val ID"])) {
return {
component: "agTextCellEditor",
}
};
if (["Date Col"].includes(params.data["Val ID"])) {
return {
component: "agDateStringCellEditor",
params: {
'useFormatter': true,
'min': '2023-01-01',
'max': '2040-12-31',
},
}
};
if (["Hours Col", "Value Col"].includes(params.data["Val ID"])) {
return {
component: "agNumberCellEditor",
}
};
if (["Bool Col"].includes(params.data["Val ID"])) {
return {
component: 'agSelectCellEditor',
params: {
values: ['Yes', 'No'],
},
popup: true,
};
};
if (["Options Col"].includes(params.data["Val ID"])) {
return {
component: 'agSelectCellEditor',
params: {
values: ['Option 1', 'Option 2', 'Other Option 1',
'Other Option 2', '3'],
},
};
};
return undefined;
};
function isFourDigits(value) {
let fourDigitsRegex = /^\d{4}$/;
return fourDigitsRegex.test(value);
}
function isBetween1950AndCurrentYear(value) {
let currentYear = new Date().getFullYear();
return value >= 1950 && value <= currentYear;
}
dagfuncs.testValue = (params) => {
if (!params.newValue) {
return false
}
if (params.data["Val ID"] == "Year Col") {
let currentYear = new Date().getFullYear();
// Check if the text is composed by 4 digits and the YEAR is between 1900 and 2022
if (!isFourDigits(params.newValue)) {
return false
}
if (!isBetween1950AndCurrentYear(params.newValue)) {
return false
}
}
params.data[params.column.colId] = params.newValue
return true
}
I’ve discovered the rowTransaction
alternative but after some tests, I didn’t understand how I could use it on my current code;
If you have any question please let me know; Any help on this will be very welcome