Column State

Column definitions have both stateful and non-stateful attributes. Stateful attributes can have
their values changed by the grid (for example, column sort can be changed by the user clicking on the column
header). Non-stateful attributes do not change from what is set in the column definition
(for example, once the header name is set as part of a column definition, it typically does not change).

The DOM also has stateful vs non-stateful attributes. For example, consider a DOM element and
setting element.style.width="100px" will indefinitely set width to 100 pixels,
the browser will not change this value. However setting element.scrollTop=200 will
set the scroll position, but the browser can change the scroll position further following user
interaction, thus scroll position is stateful as the browser can change the state.

The full list of stateful attributes of columns are represented by the columnState interface:

columnState Properties

Property Type Description
aggFunc string | IAggFunc | null The aggregation function applied
flex number | null Column’s flex if flex is set
hide boolean | null True if the column is hidden
pinned ‘left’ | ‘right’ | boolean | null Set if column is pinned
pivot boolean | null True if pivot active
pivotIndex number | null The order of the pivot, if pivoting by many columns
rowGroup boolean | null True if row group active
rowGroupIndex number | null The order of the row group, if grouping by many columns
sort ‘asc’ | ‘desc’ | null Sort applied to the column
sortIndex number | null The order of the sort, if sorting by many columns
width number Width of the column in pixels

Column State Retrieval

Use columnState to retrieve columns current state. In this example columnState is an input to the callback,
meaning the callback runs each time the state changes. Note how reordering or resizing the columns changes the current
state, which is displayed below the grid.

import json
import dash_ag_grid as dag
from dash import Dash, html, Input, Output, callback
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"},
    {"field": "athlete"},
    {"field": "age"},
    {"field": "sport"},
    {"field": "total"},
]

app.layout = html.Div(
    [
        dag.AgGrid(
            id="column-state-retrival",
            rowData=df.to_dict("records"),
            columnDefs=columnDefs,
            defaultColDef={"sortable": True, "resizable": True, "filter": True},
            columnSize="sizeToFit",
        ),
        html.Pre(id="pre-col-state",
                 style={'border': 'thin lightgrey solid', 'display': 'inline-block'}),
    ]
)


@callback(
    Output("pre-col-state", "children"),
    Input("column-state-retrival", "columnState"),
    prevent_initial_call=True,
)
def display_state(col_state):
    return json.dumps(col_state, indent=2)


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

Save and Restore Column State

Note that when columnState is set, the order of the columns is set to match the order of the newly provided Column
State, this is usually the desired and expected behaviour when we want to restore a full state.
If the desired behaviour is that column’s order should be maintained,
see Maintain the Column Order below.

The example below demonstrates saving and restoring Column State. Try the following:

import json
import dash_ag_grid as dag
from dash import Dash, html, Input, Output, State, no_update, callback
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"},
    {"field": "athlete"},
    {"field": "age"},
    {"field": "sport"},
    {"field": "total"},
]

app.layout = html.Div(
    [
        html.Div(
            [
                html.Button("Save State", id="btn-save-state"),
                html.Button("Restore State", id="btn-restore-state"),
                html.Button("Reset State", id="btn-reset-state"),
            ],
        ),
        dag.AgGrid(
            id="column-state-save-restore",
            rowData=df.to_dict("records"),
            defaultColDef={"sortable": True, "resizable": True, "filter": True, 'width': 130},
            columnDefs=columnDefs,
        ),
        html.Div("Saved columns state:"),
        html.Pre(id="pre-column-state-save-restore",
                 style={'border': 'thin lightgrey solid', 'display': 'inline-block'}),
    ]
)


@callback(
    Output("pre-column-state-save-restore", "children"),
    Input("btn-save-state", "n_clicks"),
    State("column-state-save-restore", "columnState"),
    prevent_initial_call=True,
)
def save_column_state(_, col_state):
    return json.dumps(col_state, indent=2)


@callback(
    Output("column-state-save-restore", "columnState"),
    Input("btn-restore-state", "n_clicks"),
    State("pre-column-state-save-restore", "children"),
    prevent_initial_call=True,
)
def restore_column_state(_, saved_col_state):
    return json.loads(saved_col_state) if saved_col_state else no_update


@callback(
    Output("column-state-save-restore", "resetColumnState"),
    Input("btn-reset-state", "n_clicks"),
)
def reset_column_state(_):
    # Triggers the AG Grid internal method resetColumnState() by setting dag.AgGrid.resetColumnState = True
    # Which will reapply the current 'columnDefs'
    return True


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

Apply Partial Column State

While setting columnState, it is possible to provide only partial state for each column, the properties that are not
set will keep their value, like the example below that only add the 'hide': True, keeping all other states the same.

new_state = [
    {"colId": "athlete"},
    {"colId": "sport"},
    {"colId": "country", 'hide': True},
    {"colId": "age", 'hide': True},
    {"colId": "year"},
    {"colId": "total"},
]

Note the following:
- The full column set must be provided, even if there is no state change.
- Like for a full update, the order of the columns of the new state will be applied.
See Maintain the Column Order
below to keep the current order of the columns.

Here is an example showing a partial state update, hiding the columns Country and Age.
Note that, if you modify the column order, when applying the new state, its column order is also applied, removing your
custom order.

import dash_ag_grid as dag
from dash import Dash, html, Input, Output, callback
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": "athlete"},
    {"field": "age"},
    {"field": "country"},
    {"field": "year"},
    {"field": "sport"},
    {"field": "total"},
]

app.layout = html.Div(
    [
        html.Button("Hide Age+Country", id="btn-hide"),
        dag.AgGrid(
            id="column-state-partial-update",
            rowData=df.to_dict("records"),
            defaultColDef={"sortable": True, "resizable": True, "filter": True},
            columnDefs=columnDefs,
            columnSize="sizeToFit",
        ),
    ]
)


@callback(
    Output("column-state-partial-update", "columnState"),
    Output("btn-hide", "children"),
    Input("btn-hide", "n_clicks"),
    prevent_initial_call=True,
)
def update_hide(n):
    hide_ON = n % 2 != 0
    new_state = [
        {"colId": "athlete"},
        {"colId": "sport"},
        {"colId": "country", 'hide': hide_ON},
        {"colId": "age", 'hide': hide_ON},
        {"colId": "year"},
        {"colId": "total"},
    ]
    return new_state, f"{'Show' if hide_ON else 'Hide'} Age+Country"


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

Maintain the Column Order

When we set a new columnState, the column order of the new Column State will also be applied. If the current column
order must be maintained and only few properties must be updated, the easiest way is to retrieve the current state and
modify the properties that need to be changed, then apply this modified state.

Another way to maintain the column order, particularly suited for saved state and more complex state, is to provide the
new state with the full set of columns, then re-order this new state to match the current columns order and apply this
new re-ordered state. See the
examples Save and Restore Partial Column State
and Update Column State - More examples
below.

Here is an example showing how to maintain the column order when updating the state.
Like the previous example, the columns Country and Age are hidden, but this time, the column order is
maintained.

import dash_ag_grid as dag
from dash import Dash, html, Input, Output, State, callback
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": "athlete"},
    {"field": "age"},
    {"field": "country"},
    {"field": "year"},
    {"field": "sport"},
    {"field": "total"},
]

app.layout = html.Div(
    [
        html.Button("Hide Age+Country", id="btn-hide-keep-order"),
        dag.AgGrid(
            id="column-state-keep-order",
            rowData=df.to_dict("records"),
            defaultColDef={"sortable": True, "resizable": True, "filter": True},
            columnDefs=columnDefs,
            columnSize="sizeToFit",
        ),
    ]
)


@callback(
    Output("column-state-keep-order", "columnState"),
    Output("btn-hide-keep-order", "children"),
    Input("btn-hide-keep-order", "n_clicks"),
    State("column-state-keep-order", "columnState"),
    prevent_initial_call=True,
)
def update_hide_keep_order(n, col_state):
    hide_ON = n % 2 != 0
    for col in col_state:
        col['hide'] = (hide_ON and col['colId'] in ['age', 'country'])
    return col_state, f"{'Hide' if hide_ON else 'Show'} Age+Country"


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

Save and Restore Partial Column State

The following example shows how to save a partial Column State, and how to restore only the saved properties, keeping
all other properties parameters as is. You can also choose to restore the column order or the keep the current order.

import json

import dash_ag_grid as dag
from dash import Dash, html, Input, Output, State, dcc, no_update, callback
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": "athlete"},
    {"field": "age"},
    {"field": "country"},
    {"field": "year"},
    {"field": "sport"},
    {"field": "total"},
]

app.layout = html.Div(
    [
        dcc.Checklist(
            id="chk-partial-state",
            options=[
                {"label": "Save Width", "value": "width"},
                {"label": "Save Pinned", "value": "pinned"},
                {"label": "Save Sort", "value": "sort"},
            ],
            value=["width"],
        ),
        dcc.Checklist(id="chk-restore-order", options=[{"label": "Restore Order", "value": "order"}]),
        html.Div([
            html.Button("Save Partial State", id="btn-partial-save"),
            html.Button("Restore Partial State", id="btn-partial-restore"),
        ]),

        dag.AgGrid(
            id="column-state-partial-save-restore",
            rowData=df.to_dict("records"),
            defaultColDef={"sortable": True, "resizable": True},
            columnDefs=columnDefs,
            columnSize="sizeToFit",
        ),
        html.Div('Saved Partial State:'),
        html.Pre(id='pre-saved-state',
                 style={'border': 'thin lightgrey solid', 'display': 'inline-block'}),
    ]
)


@callback(
    Output("pre-saved-state", "children"),
    Input("btn-partial-save", "n_clicks"),
    State("chk-partial-state", "value"),
    State("column-state-partial-save-restore", "columnState"),
    prevent_initial_call=True,
)
def save_sort_state(_, props_to_save, state):
    # add 'colId' and 'sortIndex' if 'sort'
    props_to_save.insert(0, 'colId')
    if 'sort' in props_to_save:
        props_to_save.append('sortIndex')

    partial_sort_state = [{prop: col[prop] for prop in props_to_save} for col in state]
    return json.dumps(partial_sort_state, indent=2)


@callback(
    Output("column-state-partial-save-restore", "columnState"),
    Input("btn-partial-restore", "n_clicks"),
    State("pre-saved-state", "children"),
    State("chk-restore-order", "value"),
    State("column-state-partial-save-restore", "columnState"),
    prevent_initial_call=True,
)
def restore_sort_state(_, saved_sort_state, restore_order, col_state):
    if not saved_sort_state:
        return no_update

    saved_sort_state = json.loads(saved_sort_state)

    if restore_order:
        return saved_sort_state

    # convert state to key-value pairs
    column_state_keys = {i['colId']: i for i in saved_sort_state}
    # re-order saved_sort_state to match the current column order
    saved_sort_state = [column_state_keys[col['colId']] for col in col_state]

    return saved_sort_state


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

Update Column State - More examples

The code below shows more examples updating the Column State.

Note that those examples maintain the column order, except, obviously the ‘New order’ button, and the ‘Complex State’
that pins Athlete on the left and Total on the right, but the order of other columns are maintained.

import dash_ag_grid as dag
from dash import Dash, html, dcc, Input, Output, State, ctx, callback
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": "athlete"},
    {"field": "age", "initialWidth": 100},
    {"field": "country"},
    {"field": "year", "initialWidth": 100},
    {"field": "sport"},
    {"field": "total", "initialWidth": 100},
]

app.layout = html.Div(
    [
        html.Div([
            html.Button("Sort Athlete", id="btn-sort-one"),
            html.Button("Sort Country > Sport (Clear others)", id="btn-sort-two"),
            html.Button("Clear all sorting", id="btn-sort-clear")
        ]),
        html.Div([
            html.Button("New Order", id="btn-order"),
            html.Div("New Order: ['Athlete', 'Sport', 'Country', 'Age', 'Year', 'Total']",
                     style={'display': 'inline-block', 'margin-left': 10}),
        ]),
        html.Div([
            dcc.Input(id="input-range-width", type="range", min=80, max=250, step=5, value=100),
            html.Div("Set {'width': 100} for numeric columns ['Age', 'Year', 'Total']",
                     id='div-width', style={'display': 'inline-block', 'margin-left': 10}),
        ]),
        html.Button("Complex State", id="btn-complex-state", style={'display': 'block'}),
        html.Button("Reset State", id="btn-reset-state-more", style={'display': 'block'}),
        dag.AgGrid(
            id="column-state-update-more",
            rowData=df.to_dict("records"),
            defaultColDef={"sortable": True, "resizable": True, "filter": True, 'initialWidth': 130},
            columnDefs=columnDefs,
        ),
    ]
)


@callback(
    Output("column-state-update-more", "columnState"),
    State("column-state-update-more", "columnState"),
    Input("btn-sort-one", "n_clicks"),
    Input("btn-sort-two", "n_clicks"),
    Input("btn-sort-clear", "n_clicks"),
    prevent_initial_call=True,
)
def update_sort(col_state, *_):
    # sort by athlete
    if ctx.triggered_id == 'btn-sort-one':
        for col in col_state:
            if col['colId'] == 'athlete':
                col['sort'] = 'asc'

    # sort by country then sport, clear others
    elif ctx.triggered_id == 'btn-sort-two':
        for col in col_state:
            if col['colId'] == 'country':
                col['sort'] = 'asc'
                col['sortIndex'] = 0
            elif col['colId'] == 'sport':
                col['sort'] = 'asc'
                col['sortIndex'] = 1
            else:
                col['sort'] = None

    # clear all sort
    else:
        for col in col_state:
            col['sort'] = None

    return col_state


@callback(
    Output("column-state-update-more", "columnState", allow_duplicate=True),
    Output("btn-order", "children"),
    Input("btn-order", "n_clicks"),
    prevent_initial_call=True,
)
def update_order(n):
    if n % 2 == 0:
        return [
            {"colId": "athlete"},
            {"colId": "age"},
            {"colId": "country"},
            {"colId": "year"},
            {"colId": "sport"},
            {"colId": "total"},
        ], 'New Order'
    return [
        {"colId": "athlete"},
        {"colId": "sport"},
        {"colId": "country"},
        {"colId": "age"},
        {"colId": "year"},
        {"colId": "total"},
    ], 'Initial Order'


@callback(
    Output("column-state-update-more", "columnState", allow_duplicate=True),
    Output("div-width", "children"),
    Input("input-range-width", "value"),
    State("column-state-update-more", "columnState"),
    prevent_initial_call=True,
)
def update_width(width, col_state):
    for col in col_state:
        if col['colId'] in ['age', 'year', 'total']:
            col['width'] = width
    return col_state, f"Set {{'width': {width}}} for numeric columns ['Age', 'Year', 'Total']"


@callback(
    Output("column-state-update-more", "columnState", allow_duplicate=True),
    Input("btn-complex-state", "n_clicks"),
    State("column-state-update-more", "columnState"),
    prevent_initial_call=True,
)
def update_complex_state(_, col_state):
    complex_state = [
        {"colId": "athlete", "width": 200, "pinned": "left", "sort": None, "sortIndex": None},
        {"colId": "sport", "width": 200, "pinned": None, "sort": None, "sortIndex": None},
        {"colId": "country", "width": 200, "pinned": None, "sort": None, "sortIndex": None},
        {"colId": "year", "width": 96, "pinned": None, "sort": None, "sortIndex": None},
        {"colId": "age", "width": 116, "pinned": None, "sort": "asc", "sortIndex": 1},
        {"colId": "total", "width": 124, "pinned": "right", "sort": "desc", "sortIndex": 0}
    ]

    # convert state to key-value pairs
    column_state_keys = {i['colId']: i for i in complex_state}
    # re-order complex_state to match the current column order
    complex_state = [column_state_keys[col['colId']] for col in col_state]

    return complex_state


@callback(
    Output("column-state-update-more", "resetColumnState"),
    Output("input-range-width", "value"),
    Input("btn-reset-state-more", "n_clicks"),
    prevent_initial_call=True,
)
def reset_column_state(_):
    # Triggers the AG Grid internal method resetColumnState() by setting dag.AgGrid.resetColumnState = True
    # Which will reapply the current 'columnDefs'
    return True, 100


if __name__ == "__main__":
    app.run(debug=True)
New Order: ['Athlete', 'Sport', 'Country', 'Age', 'Year', 'Total']
Set {'width': 100} for numeric columns ['Age', 'Year', 'Total']

Width and Flex

When flex is active on a Column, the grid ignores the width attribute when setting the width.

When getting columnState, both width and flex are returned. When setting columnState, if flex is present
then width is ignored.

If you want to restore a Column’s width to the exact same pixel width as specified in the Column State,
set flex=None for that Column’s state to turn Flex off.