AG Grid - Persistence with Buttons/Refresh and Add Column

Hi All,

I am having issues with persistence, especially when using buttons.

I found it hard to explain, so thought it best to provide some code and usage to show the issues I am having

Issues

  1. Move around columns, change width, apply sort and filter. If page is refreshed by browser (or even close tab and reopen) persistence works as expected.
    However if you use the ‘Load View’ button the order of the columns and their widths revert back to normal, but filter and sort are preserved.
    What make this especially weird if that both refreshes are done by the same ‘load_layout()’ method

2a. Input ‘Year’ into input box and press ‘Add Column’ this will unhide the applicable column. Persistence continues to work as above.
2b. Input ‘Year’ into input box and press ‘Add Column’ this will unhide the applicable column, repeat with ‘Sport’ and the second column will appear. Persistence continues to work as above.
2c. Input ‘Year’ into input box and press ‘Add Column’ this will unhide the applicable column. Refresh webpage and will stay. Now enter Input ‘Sport’ into input box and press ‘Add Column’, year goes and sport appears
I am not sure what is happening with the behavior in 2c

  1. Finally Does anyone have a better way of adding adhoc columns by field name? I had previously tried getting the columnDefs and appending a new field dictionary before returning, but I could not get the columns to persist on refresh

Thank you

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

app = Dash(__name__)

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

columnDefs = [
    {"field": "country"},
    {"field": "year", 'hide': True},
    {"field": "athlete"},
    {"field": "sport", 'hide': True},
    {"field": "total"},
]

row_data = df.reset_index().to_dict("records")

def load_layout():
    children = [
        dag.AgGrid(
            id="grid",
            columnSize="sizeToFit",
            rowData=row_data,
            columnDefs=columnDefs,
            defaultColDef={"filter": True, "sortable": True, "resizable": True},
            persistence=True,
            persisted_props=["filterModel", "columnState"]
        )
    ]

    return children

app.layout = html.Div([
    html.Button("Load View", id="filter-model-btn", n_clicks=0),
    dcc.Input(id='input', placeholder='Column Name', debounce=True),
    html.Button("Add Column", id="add-btn", n_clicks=0),
    html.Div(
        [
        ],
        id='layout'
    )
    ])


@callback(
    Output("layout", "children"),
    Input("filter-model-btn", "n_clicks"),
)
def layout(n):
    if n >0:
        return load_layout()
    return load_layout()

@callback(
    Output('grid', "columnDefs"),
    Input('input', 'value'),
    Input("add-btn", "n_clicks"),
    State('grid', "columnDefs"),
    prevent_initial_call=True
)
def add(text, n, cols):
    print(cols)
    for col in cols:
        if col['field'] == text.lower():
            col['hide'] = False

    return cols


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

Hello @QSD449,

Have you tried using columnState instead of columnDefs?

1 Like

After looking at your example and messing around with it, you are experiencing the issue between the columnState and columnDefs.

The columnDefs trump the columnState when passed alone, but the columnState will trump the columnDefs when passed together or alone.

The reason your columnDefs dont work in the instance you demonstrated, when you are adjusting the one it isnt overwriting the old version of the columnDefs.

I think this version will tick all the boxes for you:

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

app = Dash(__name__, suppress_callback_exceptions=True)

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

columnDefs = [
    {"field": "country"},
    {"field": "year", 'hide': True},
    {"field": "athlete"},
    {"field": "sport", 'hide': True},
    {"field": "total"},
]

row_data = df.reset_index().to_dict("records")

def load_layout():
    children = [
        dag.AgGrid(
            id="grid",
            columnSize="sizeToFit",
            rowData=row_data,
            columnDefs=columnDefs,
            defaultColDef={"filter": True, "sortable": True, "resizable": True, "sort": None},
            persistence=True,
            persisted_props=["filterModel", "columnState"],
            filterModel={}
        )
    ]

    return children

app.layout = html.Div([
    html.Button("Load View", id="filter-model-btn", n_clicks=0),
    dcc.Input(id='input', placeholder='Column Name', debounce=True),
    html.Button("Add Column", id="add-btn", n_clicks=0),
    html.Div(
        [
        ],
        id='layout'
    )
    ])


@callback(
    Output("layout", "children"),
    Input("filter-model-btn", "n_clicks"),
)
def layout(n):
    if n >0:
        return load_layout()
    return load_layout()

@callback(
    Output('grid', "columnDefs"),
    Input('input', 'value'),
    Input("add-btn", "n_clicks"),
    State('grid', "columnDefs"),
    State('grid', "columnState"),
    prevent_initial_call=True
)
def add(text, n, cols, states):
    for col in cols:
        if col['field'] == text.lower():
            col['hide'] = False
        for state in states:
            if state['colId'] == col['field'] and state['colId'] != text.lower():
                col['hide'] = state['hide']
                col['sort'] = state['sort']

    return cols


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

Hi @jinnyzor

Thank you for your reply.
The updated code works as I had hoped for Q2, adding the columns.

RE Q1, Do you know why the persistence is not maintained fully (i.e. sort*, column widths, column order and unhidden columns) when using the ‘Load View’ button but they are when using page refresh?

@callback(
    Output("layout", "children"),
    Input("filter-model-btn", "n_clicks"),
)
def layout(n):
    if n >0:
        return load_layout()
    return load_layout()

It is really confusing as they are the same function

*this can be fixed but removing the added ‘“sort”: None’ property from defaultColDef

I have also noticed that column order is reverted when a new column is added (unhidden)
I assume because the order of the columnState is not updated

Again, those are things from the columnDefs, because you aren’t changing the order or the widths it is overwriting the columnState.

I thought for Q1 you wanted to revert to the original columnDefs completely, if not then you need to be using columnState directly.

Then the issue is that persistence cannot be maintained unless it is triggered by the client, if done by a callback, the persistence is deleted.

1 Like

For Q1 I am trying to understand why when the webpage is refreshed within the browser the persistence is maintained, but when I use the button parts of the persistence are lost.

Do I need to use State and Output for columnState like this:

@callback(
    Output("layout", "children"),
    Output('grid', "columnState"),
    Input("filter-model-btn", "n_clicks"),
    State('grid', "columnState"),
)
def layout(n, state):
    if n >0:
        return load_layout(), state
    return load_layout(), state

The problem is I get the below error message, As the grid has not loaded/been generated.
A nonexistent object was used in an Output of a Dash callback. The id of this object is grid and the property is columnState. The string ids in the current layout are: [filter-model-btn, input, add-btn, layout]

I can overcome the error by adding a starting grid, see below, but then I lose the persistence when I refresh, but keep it with button click.
I would like a way to keep all persistence with both refresh and button click

start_grid = dag.AgGrid(
            id="grid",
            columnSize="sizeToFit",
            rowData=row_data,
            columnDefs=columnDefs,
            defaultColDef={"filter": True, "sortable": True, "resizable": True, "sort": None},
            persistence=True,
            persisted_props=["filterModel", "columnState"],
            filterModel={}
        )

app.layout = html.Div([
    html.Button("Load View", id="filter-model-btn", n_clicks=0),
    dcc.Input(id='input', placeholder='Column Name', debounce=True),
    html.Button("Add Column", id="add-btn", n_clicks=0),
    html.Div(
        [
            start_grid
        ],
        id='layout'
    )
    ])

When you are clicking the button, what do you want to see the grid do exactly?

No change at all? Is the button to reload new data?

I don’t understand the point of the button.

1 Like

I want the button to act the same as refresh.

The point of the button would be to go from one view to another view, see quick example at bottom.

It seems to behave most of the time, but the issue seems to be if you press the ‘Main View’ button whilst on the main view
(edit) the persistence is removed

Can I prevent that from happening?
I guess I could use one button that updates text and id when its changing between screens of hiding the button when the main view is being shown.

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

app = Dash(__name__, suppress_callback_exceptions=True)

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

columnDefs = [
    {"field": "country"},
    {"field": "year", 'hide': True},
    {"field": "athlete"},
    {"field": "sport", 'hide': True},
    {"field": "total"},
]

row_data = df.reset_index().to_dict("records")

def load_layout():
    children = [
        dag.AgGrid(
            id="grid",
            columnSize="sizeToFit",
            rowData=row_data,
            columnDefs=columnDefs,
            defaultColDef={"filter": True, "sortable": True, "resizable": True, "sort": None},
            persistence=True,
            persisted_props=["filterModel", "columnState"],
            filterModel={}
        )
    ]

    return children

def other_layout():
    children = [
        html.P("Hello World!")
    ]

    return children

# start_grid = dag.AgGrid(
#             id="grid",
#             columnSize="sizeToFit",
#             rowData=row_data,
#             columnDefs=columnDefs,
#             defaultColDef={"filter": True, "sortable": True, "resizable": True, "sort": None},
#             persistence=True,
#             persisted_props=["filterModel", "columnState"],
#             filterModel={}
#         )

app.layout = html.Div([
    html.Button("Main View", id="main-btn", n_clicks=0),
    html.Button("Other View", id="other-btn", n_clicks=0),
    dcc.Input(id='input', placeholder='Column Name', debounce=True),
    html.Button("Add Column", id="add-btn", n_clicks=0),
    html.Div(
        [
            # start_grid
        ],
        id='layout'
    )
    ])


@callback(
    Output("layout", "children"),
    Input("main-btn", "n_clicks"),
    Input("other-btn", "n_clicks"),
)
def layout(main, other):
    ctx = callback_context

    if ctx.triggered_id is None:
        return load_layout()
    elif ctx.triggered_id == 'main-btn':
        return load_layout()
    elif ctx.triggered_id == 'other-btn':
        return other_layout()
    else:
        return no_update


@callback(
    Output('grid', "columnDefs"),
    Input('input', 'value'),
    Input("add-btn", "n_clicks"),
    State('grid', "columnDefs"),
    State('grid', "columnState"),
    prevent_initial_call=True
)
def add(text, n, cols, states):
    for state_idx, state in enumerate(states):
        for col_idx, col in enumerate(cols):
            if state['colId'] == col['field']:
                match = col_idx
                temp = cols[state_idx]
                cols[state_idx] = cols[match]
                cols[match] = temp
                col['hide'] = state['hide']
                col['sort'] = state['sort']
            if col['field'] == text.lower():
                col['hide'] = False

    return cols


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

If the columns available are never changing, you should be saving the columnState and not the columnDefs.

Each time you load the columnDefs via a callback without the columnState present, it is blasting the previous columnState.

What you can do in your case is store the columnState in a dcc.store and use its state in the callback and pass it to the load_layout function.

1 Like