Column selection in Dash AG Grid

Hi,

is there a way to make Dash AG Grid columns selectable as it is for rows?

Hello @subodhpokhrel7,

There isn’t something to select an entire column.

What is your use-case?

Hi @jinnyzor ,

Thank you.
I’d like the user to be able to select two (out of multiple) columns for mathematical calculation for each cells in each row. Selecting from the header row should also work, but the whole column being highlighted when selected would make it visually better.

You could make the visual change by using cellClassRules styling and headerClass as well. You can add checkboxes as well, then build a dcc.Store that maintains your chosen columns and use that in your class functions.

Hi @jinnyzor ,

is there a way to add checkboxes to headers and not make it look like the whole table is selected when any box is selected?
If not, can a separate row be added with checkboxes?

I’m trying to do the styling of columns on selection here:

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

df = pd.read_csv("https://raw.githubusercontent.com/plotly/datasets/master/solar.csv")

app = Dash(__name__)

grid = dag.AgGrid(
    id="cell-selection-simple-click-callback",
    rowData=df.to_dict("records"),
    columnDefs=[{"field": i} for i in df.columns],
    columnSize="sizeToFit",
    getRowId="params.data.State"
)

app.layout = html.Div([grid, html.Pre(id="pre-cell-selection-simple-click-callback")])


@callback(
    Output("pre-cell-selection-simple-click-callback", "children"),
    Input("cell-selection-simple-click-callback", "cellClicked")
)
def display_cell_clicked_on(cell):
    return f"Clicked on cell:\n{json.dumps(cell, indent=2)}" if cell else "Click on a cell"



@callback(
    Output('cell-selection-simple-click-callback', 'columnDefs'),
    Input('cell-selection-simple-click-callback', 'cellClicked'),
    State('cell-selection-simple-click-callback', 'columnDefs'),
    prevent_initial_call=True
)
def change_style(cell, col_def):

    field= cell['colId']

    for item in col_def:
        if item['field'] == field:
            item['cellStyle'] = {'background-color': '#66c2a5'}

        
    

    return col_def
    

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

Sure, here you go:

css file:

.headerSelected {
    background-color: #66c2a5
}

js file:

const {useImperativeHandle, useRef, useState, useEffect, forwardRef, Component} = React
var dagfuncs = window.dashAgGridFunctions = window.dashAgGridFunctions || {};
var dagcomponentfuncs = window.dashAgGridComponentFunctions = window.dashAgGridComponentFunctions || {};

dagcomponentfuncs.SelectColumn = function (props) {
    const [checked, setChecked] = useState(JSON.parse(localStorage.getItem("headerSelections")).includes(props.column.colId))
    return React.createElement('div',
        {}, [
        React.createElement(
            'input',
            {
                onChange: () => {
                    setChecked(!checked)
                    selects = JSON.parse(localStorage.getItem("headerSelections"))
                    if (!checked) {
                        selects.push(props.column.colId)
                    } else {
                        for (y=0; y<selects.length; y++) {
                            if (selects[y] == props.column.colId) {
                                selects.splice(y, 1)
                                break
                            }
                        }
                    }
                    localStorage.setItem("headerSelections", JSON.stringify(selects))
                    document.getElementById('headerSelections_sync').click()
                    props.api.refreshCells()
                },
                className: props.className,
                style: props.style,
                type: "checkbox",
                checked: checked || false,
            }),
            props.displayName]
        )
}

dagfuncs.testSelections = (params) => {
    selections = JSON.parse(localStorage.getItem('headerSelections'))
    if (selections.includes(params.column.colId)) {
        return true
    }
    return false
}

app.py:

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

df = pd.read_csv("https://raw.githubusercontent.com/plotly/datasets/master/solar.csv")

app = Dash(__name__)

grid = dag.AgGrid(
    id="cell-selection-simple-click-callback",
    rowData=df.to_dict("records"),
    columnDefs=[{"field": i, "headerComponent": "SelectColumn", "cellClassRules": {"headerSelected": "testSelections(params)"}} for i in df.columns],
    columnSize="sizeToFit",
    getRowId="params.data.State"
)

app.layout = html.Div([grid, html.Pre(id="pre-cell-selection-simple-click-callback"),
                       dcc.Store(id='headerSelections', storage_type='local', data=[]),
                       html.Button(id='headerSelections_sync', style={'display': 'none'})])

app.clientside_callback(
    """function (n) {
        return JSON.parse(localStorage.getItem('headerSelections'))
    }""",
    Output('headerSelections', 'data'),
    Input('headerSelections_sync', 'n_clicks')
)


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

Only issue with this is that you would technically need to add sort and filter handlers, but only if you use them.

1 Like

Hi @jinnyzor ,

It works great. Thank you.
I have two follow up questions though, is there a way to limit the selections? Let’s say up to 2 columns.
Also, it is not possible to use the cellClicked property of AG Grid anymore to see the column ID of the selected (and there isn’t a headerClicked or similar). Is there another way how I could see/store the column ID of the selections?

The column selections are stored into a dcc.Store that you can access at any time. :grin:

And yes, you can make it so only two are selected at a time. You just need to figure out how that is restricted. Does the next click remove one and add the other, etc.

1 Like

I had tried to print the selections after each clicks with Input('headerSelections_sync', 'n_clicks'), but it did not work as intended. Adding a continue button worked fine and printing the selections after click works fine. The limit to selections can now be added based of number in dcc.Store.

Thank you very much for your help

1 Like

Hi @jinnyzor ,

I want to revisit this as I have run into a slight problem.
The storage of the selected columns in localStorage causes persistence and also doesn’t work when run in incognito mode (or when cache is cleared or run in a new browser) with Cannot read properties of null (reading 'includes') as the error message. Is there a way to solve this issue?

I have tried replacing localStorage with sessionStorage, but to no avail.

Did you try replacing all local in the code with session?

Yes, in both of the files as well.

Instead of using a storage, you could always just use a variable on the browser, you lose persistence upon refresh though.

A post was split to a new topic: Data Selection from AG Grid Column

Hi, Thank you for the guide how to use the “headerComponent”.

However, if I wanted to apply sorting when clicking the column name as before, could you tell me how to do it?

my defaultColDef has set {“sortable”: True}

Hello @wowwwn,

To add a sort handler, there must be an event handler inside the component that will trigger the grids sort to adjust.

You can check out more info here:

https://www.ag-grid.com/react-data-grid/column-headers/

Thanks for the reply,

I found out how to apply sorting by clicking on a column header with the help of ChatGPT.

const {useImperativeHandle, useRef, useState, useEffect, forwardRef, Component} = React
var dagcomponentfuncs = window.dashAgGridComponentFunctions = window.dashAgGridComponentFunctions || {};

dagcomponentfuncs.SelectColumn = function (props) {
    const [checked, setChecked] = useState(JSON.parse(localStorage.getItem("headerSelections")).includes(props.column.colId))

    const [sortState, setSortState] = useState('');

    const onCheckboxChange = () => {
        setChecked(!checked);
        let selects = JSON.parse(localStorage.getItem("headerSelections"));
        if (!checked) {
            selects.push(props.column.colId);
        } else {
            selects = selects.filter(id => id !== props.column.colId);
        }
        localStorage.setItem("headerSelections", JSON.stringify(selects));
        document.getElementById('headerSelections_sync').click();
        props.api.refreshCells();
    };

    const onHeaderClick = (event) => {
        if (event.target.type !== 'checkbox') {

            let currentSort = props.column.getSort();
            let nextSortState;
    
            if (currentSort === 'asc') {
                nextSortState = 'desc';
                setSortState('desc');
            } else if (currentSort === 'desc') {
                nextSortState = null;
                setSortState('none'); 
            } else {
                nextSortState = 'asc';
                setSortState('asc'); 
            }

            props.api.applyColumnState({
                state: [{colId: props.column.colId, sort: nextSortState}],
            });
        }
    };

    const imagePath = (state) => {
        switch(state) {
            case 'asc': return 'assets/icons/bx-sort-up.png';
            case 'desc': return 'assets/icons/bx-sort-down.png';
            case 'none': return '';
            default: return '';
        }
    };


    return React.createElement('div',
        { onClick: onHeaderClick }, [
        React.createElement(
            'input',
            {
                onChange: onCheckboxChange,
                className: props.className,
                style: props.style,
                type: "checkbox",
                checked: checked || false,
            }),
            React.createElement('span', {style: { marginLeft: '8px'}}, props.displayName),
            
            sortState && React.createElement('img', { src: imagePath(sortState), style: { marginLeft: '8px' } }), 
        ]
        )
}

But still there is an issue that i’m struggling with.

I want to deselects all column checks, or reset all column sorting options by clicking a button which will trigger dash callback.