Sure, here is something you can do:
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"
)
df.reset_index(inplace=True)
df.rename(columns={"index": "InputRowId"}, inplace=True)
rowData = df.to_dict('records')
columnDefs = [
# Row group by country and by year is enabled.
{"field": "country", "sortable": True, "filter": True, "rowGroup": True, "hide": True},
{"field": "year", "sortable": True, "filter": True, "rowGroup": True, "hide": 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, 'editable': True},
{"field": "total", "sortable": True, "filter": True, "aggFunc": "sum"},
{"field": "InputRowId", "sortable": True, "filter": True, 'hide': True, 'suppressColumnsToolPanel': True},
]
def extractRowsFromData(request, df):
response = []
print(request)
groupBy = []
dff = df.copy()
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},
defaultColDef=dict(
resizable=True,
enableRowGroup=True,
enableValue=True,
enablePivot=True
),
enableEnterpriseModules=True,
rowModelType="serverSide",
style={'overflow': 'auto', 'resize': 'both'}
),
]
)
app.layout = html.Div(
[
dcc.Markdown("Example: Organisational Hierarchy using Tree Data "),
grid,
html.Div(id='hidden-div', style={'display': 'hidden'}),
dcc.Store(id='stored_changes', data={}, storage_type='local')
]
)
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')
)
app.clientside_callback(
"""function (wuu, changes) {
if (wuu) {
newChanges = JSON.parse(JSON.stringify(changes))
wuu.forEach((c) => {
if (!newChanges[c['data']['InputRowId']]) {
newChanges[c['data']['InputRowId']] = {}
}
newChanges[c['data']['InputRowId']][c['colId']] = c['value']
})
return newChanges
}
return window.dash_clientside.no_update
}""",
Output('stored_changes', 'data'),
Input('grid', 'cellValueChanged'),
State('stored_changes', 'data'),
)
if __name__ == "__main__":
app.run(debug=True)
assets/.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) => {
var result = await getServerData(params.request)
// loads stored data
stored_changes = JSON.parse(localStorage.getItem('stored_changes'))
newRowData = result.rowData.map((row) => {
if (row?.InputRowId) {
if (stored_changes[row?.InputRowId]) {
return {...row, ...stored_changes[row?.InputRowId]}
}
}
return row
})
// sets up response for with new data
result['rowData'] = newRowData
setTimeout(function () {
params.success(result);
}, 200);
},
};
return dataSource;
}
Now, lets break it down, I adjusted your code for the extractRowsFromData
since it is unnecessary for the altered data to be additionally pushed to the server, instead, I am storing the changes, locally right now, could alter to server side. Then instead of createServerSideDatasource
I replace the stored changed values into the response from the server. This should keep your costs down since you wont be transferring the data as often.
You could store these as a window variable, but I like the store, because if you wanted to, you could push these changes to the server data and update a db from the change log. The window variable could also be configured to do this, it would just take a little stepping and probably creating another flask route to hand the data easier.
Let me know your thoughts.