Ag-Grid Custom Filtering

Hello All,

I’ve been trying to understand this for a while, and before I continue I’d like to know if dash ag-grid supports what I’m looking for.

If these can be done in the current version of dash ag-grid, could someone provide a small example of each? I am not a React developer so I’m having a hard time translating the official Ag-Grid docs.

Thanks!

Hi @robertf

Yes Dash AG Grid supports both – but it does require a little React of JavaScript to customize. If you give more details about what you are trying to do, it will be easier to help.

1 Like

Hello @AnnMarieW

I have an example app below that I believe can use a custom filter component if possible (using textMatcher was just an idea in case it can also work somehow). What I’m wanting to do is very niche so I understand if the reasoning doesn’t make sense. For this example, I’m wanting to show a parent row with all of its children if a filter for a column passes. So if I filter the “Employment Type” column for “contains Contract” I want to see everyone under Brittany Hanson and Francis Strickland. (I know it seems weird, but I do want to see everyone under the parent.)

import dash_ag_grid as dag
from dash import Dash, Input, Output, html, dcc
import dash_bootstrap_components as dbc
import os

app = Dash(__name__)


rowData = [
    {
        "orgHierarchy": ["Erica Rogers"],
        "jobTitle": "CEO",
        "employmentType": "Permanent",
    },
    {
        "orgHierarchy": ["Erica Rogers", "Malcolm Barrett"],
        "jobTitle": "Exec. Vice President",
        "employmentType": "Permanent",
    },
    {
        "orgHierarchy": ["Erica Rogers", "Esther Baker"],
        "jobTitle": "Director of Operations",
        "employmentType": "Permanent",
    },
    {
        "orgHierarchy": [
            "Brittany Hanson",
        ],
        "jobTitle": "Fleet Coordinator",
        "employmentType": "Permanent",
    },
    {
        "orgHierarchy": [
            "Brittany Hanson",
            "Leah Flowers",
        ],
        "jobTitle": "Parts Technician",
        "employmentType": "Contract",
    },
    {
        "orgHierarchy": [
            "Brittany Hanson",
            "Tammy Sutton",
        ],
        "jobTitle": "Service Technician",
        "employmentType": "Contract",
    },
    {
        "orgHierarchy": [
            "Brittany Hanson",
            "Derek Paul",
        ],
        "jobTitle": "Inventory Control",
        "employmentType": "Permanent",
    },
    {
        "orgHierarchy": ["Francis Strickland"],
        "jobTitle": "VP Sales",
        "employmentType": "Permanent",
    },
    {
        "orgHierarchy": [
            "Francis Strickland",
            "Morris Hanson",
        ],
        "jobTitle": "Sales Manager",
        "employmentType": "Permanent",
    },
    {
        "orgHierarchy": [
            "Francis Strickland",
            "Todd Tyler",
        ],
        "jobTitle": "Sales Executive",
        "employmentType": "Contract",
    },
    {
        "orgHierarchy": [
            "Francis Strickland",
            "Bennie Wise",
        ],
        "jobTitle": "Sales Executive",
        "employmentType": "Contract",
    },
    {
        "orgHierarchy": [
            "Francis Strickland",
            "Joel Cooper",
        ],
        "jobTitle": "Sales Executive",
        "employmentType": "Permanent",
    },
]

grid = html.Div(
    [
        dag.AgGrid(
            id="tree-data-example",
            columnDefs=[
                # we're using the auto group column by default!
                {"field": "jobTitle"},
                {"field": "employmentType"},
            ],
            defaultColDef={
                "flex": 1,
                "filter": 'agTextColumnFilter'
            },
            dashGridOptions={
                "autoGroupColumnDef": {
                    "headerName": "Organisation Hierarchy",
                    "minWidth": 300,
                    "cellRendererParams": {
                        "suppressCount": True,
                    },
                },
                "groupDefaultExpanded": -1,
                "getDataPath": {"function": "getDataPath(params)"},
                "treeData": True,
            },
            rowData=rowData,
            enableEnterpriseModules=True,
            licenseKey = os.environ['AGGRID_ENTERPRISE'],
            style={'height': 600}
        ),
    ]
)

app.layout = html.Div(
    [
        dcc.Markdown("Example: Organisational Hierarchy using Tree Data "),
        grid,
    ]
)

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

To run this example, include the following in the dashAgGridFunctions.js file in your app’s assets folder

var dagfuncs = window.dashAgGridFunctions = window.dashAgGridFunctions || {};

dagfuncs.getDataPath = function (data) {
    return data.orgHierarchy;
}

Thanks for any direction you can provide!

Ah yes - the Tree data is an AG Grid Enterprise feature. I’m not sure if there is an easy workaround to do this in the community version.

So for this I have been using the enterprise version of Ag Grid (so this is ok for the example I provided). I can’t think of a good example to provide that doesn’t use tree data.
Maybe something silly like, if someone filters the “Employment Type” column, the custom filter component will instead filter the “Job Title” column. The example code above can be used except delete references to using the enterprise version of ag-grid.

I’m mostly looking for a skeleton on how to implement what I’m looking for with tree data. The attempts I’ve made so far haven’t worked out since I don’t understand the Ag-Grid to Dash Ag-Grid conversion.

Oh, sorry - I didn’t look closely at your example. I saw the tree data and thought the issue was that you were trying to use an Enterprise feature with Community.

Since you have Enterprise, I think there are easier ways to accomplish this rather than using custom filters (although that might work too). I have some ideas but I’d like to try them first. I’m out of the office today - I’ll post what I find in a bit.

Oh no worries! I appreciate any help or leads with this. I’ll continue to look into a solution as well and will post something if I am able to figure something out.

Tree list filters is one of the things that might work: React Data Grid: Set Filter - Tree List

Thanks Ann. I tested out the suggestion but wasn’t able to get anywhere. I am going to try and recreate the example they used here for Filter Components: React Data Grid: Filter Component so that I directly compare to a working example.

Hi @robertf

I’m not sure I understand your use-case exactly. It would be helpful if you could create an example, even if it was in excel. I’m concerned that users might find it confusing if the grid sorted on a different column than where the filter was entered.

Hello Ann. So I went ahead and made a new example with a simpler use case. I am copying the first example from the Ag-Grid Docs page: JavaScript Data Grid: Filter Component
I am stuck on getting the YearFilter to work exactly as they did. I believe I need to convert their raw html code into something more compatible with Dash, so I am using dash_core_components for the radio-items. After I learn more about the method to get this to work, I can then tackle the PersonFilter.

from dash import Dash, html
import dash_ag_grid as dag
import pandas as pd

app = Dash(__name__)


df = pd.read_json('https://www.ag-grid.com/example-assets/olympic-winners.json', convert_dates=False)

rowData = df.to_dict('records')

columnDefs = [
    # { 'field': 'athlete', 'minWidth': 150, 'filter': 'PersonFilter' },
    { 'field': 'age', 'filter': 'agNumberColumnFilter' },
    { 'field': 'country', 'minWidth': 150 },
    { 'field': 'year', 'filter': 'YearFilter' },
    {
      'field': 'date',
      'minWidth': 130,
      'filter': 'agDateColumnFilter',
      'filterParams': {
        'comparator': {'function':'dateComparator'},
      },
    },
    { 'field': 'sport' },
    { 'field': 'gold', 'filter': 'agNumberColumnFilter' },
    { 'field': 'silver', 'filter': 'agNumberColumnFilter' },
    { 'field': 'bronze', 'filter': 'agNumberColumnFilter' },
    { 'field': 'total', 'filter': 'agNumberColumnFilter' },
]

defaultColDef = {
    'editable': True,
    'sortable': True,
    'flex': 1,
    'minWidth': 100,
    'filter': True,
    'resizable': True,
}


app.layout = html.Div(
    [
        dag.AgGrid(
            id="filter-component-example",
            columnDefs=columnDefs,
            rowData=rowData,
            defaultColDef=defaultColDef
        ),
    ]
)


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

dashAgGridFunctions.js

var dagfuncs = window.dashAgGridFunctions = window.dashAgGridFunctions || {};

dagfuncs.dateComparator = function (filterLocalDateAtMidnight, cellValue) {

    const dateAsString = cellValue;
    const dateParts = dateAsString.split('/');
    const cellDate = new Date(
        Number(dateParts[2]),
        Number(dateParts[1]) - 1,
        Number(dateParts[0])
    );
    if (filterLocalDateAtMidnight.getTime() === cellDate.getTime()) {
        return 0;
    }
    if (cellDate < filterLocalDateAtMidnight) {
        return -1;
    }
    if (cellDate > filterLocalDateAtMidnight) {
        return 1;
    }

}

dashAgGridComponentFunctions.js

var dagcomponentfuncs = (window.dashAgGridComponentFunctions = window.dashAgGridComponentFunctions || {});



dagcomponentfuncs.YearFilter = function (params) {

    const filterActive = false;

    function onChanged() {
        filterActive = this.checked;
        params.filterChangedCallback();
      }
    
    function  doesFilterPass(params) {
        return params.data.year >= 2010;
      }
    
    function  isFilterActive() {
        return filterActive;
      }

    return React.createElement(
        window.dash_core_components.RadioItems,
        {
            options:[
                {'label': 'All', 'value': 'all'},
                {'label': 'Since 2010', 'value': '2010'},
            ],
            value:'all',
            onChange:onChanged
        },
        null
        )
};

Hello @AnnMarieW

With the latest example I provided, does it look like I’m on the right track to get the YearFilter to work? I’m having trouble understanding how to make the RadioItems function as the example docs did.

Hi @robertf

Yes, you made a good start. I also got stuck on making the RadioItems function. This works slightly different from components in cell.

I think we need some guidance from the Dash AG Grid guru @jinnyzor

Hello @robertf,

There isnt currently a way to do this, but I have a PR pending that will fix it so that you can do something like this:

app.py

from dash import Dash, html
import dash_ag_grid as dag
import pandas as pd

app = Dash(__name__)

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

rowData = df.to_dict('records')

columnDefs = [
    { 'field': 'age', 'filter': 'agNumberColumnFilter' },
    { 'field': 'country', 'minWidth': 150 },
    { 'field': 'year', 'filter': {'function': 'YearFilter'}},
    {
      'field': 'date',
      'minWidth': 130,
      'filter': 'agDateColumnFilter',
      'filterParams': {
        'comparator': {'function':'dateComparator'},
      },
    },
    { 'field': 'sport' },
    { 'field': 'gold', 'filter': 'agNumberColumnFilter' },
    { 'field': 'silver', 'filter': 'agNumberColumnFilter' },
    { 'field': 'bronze', 'filter': 'agNumberColumnFilter' },
    { 'field': 'total', 'filter': 'agNumberColumnFilter' },
]

defaultColDef = {
    'editable': True,
    'sortable': True,
    'flex': 1,
    'minWidth': 100,
    'filter': True,
    'resizable': True,
}


app.layout = html.Div(
    [
        html.H3("See the custom filter component in the Year column"),
        dag.AgGrid(
            id="grid",
            columnDefs=columnDefs,
            rowData=rowData,
            defaultColDef=defaultColDef
        ),
    ]
)


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

dashAgGridFunctions.js

This example was adapted from React Data Grid: Filter Component
The only differences are:

  • React.createElement instead of JSX
  • setProps, which all Dash components use to report user interactions, nstead of a plain js event handler
var dagfuncs = window.dashAgGridFunctions = window.dashAgGridFunctions || {};

const [useImperativeHandle, useState, useEffect, forwardRef, Component] = [React.useImperativeHandle, React.useState, React.useEffect, React.forwardRef, React.component]

dagfuncs.YearFilter = forwardRef((props, ref) => {
   const [year, setYear] = useState('All');

   useImperativeHandle(ref, () => {
       return {
           doesFilterPass(params) {
               return params.data.year >= 2010;
           },

           isFilterActive() {
               return year === '2010'
           },

           // this example isn't using getModel() and setModel(),
           // so safe to just leave these empty. don't do this in your code!!!
           getModel() {
           },

           setModel() {
           }
       }
   });

   useEffect(() => {
       props.filterChangedCallback()
   }, [year]);

    setProps = (props) => {
        if (props.value) {
            setYear(props.value)
        }
    }

    return React.createElement(
        window.dash_core_components.RadioItems,
        {
            options:[
                {'label': 'All', 'value': 'All'},
                {'label': 'Since 2010', 'value': '2010'},
            ],
            value: year,
            setProps
        }
        )
});

dagfuncs.dateComparator = function (filterLocalDateAtMidnight, cellValue) {

    const dateAsString = cellValue;
    const dateParts = dateAsString.split('/');
    const cellDate = new Date(
        Number(dateParts[2]),
        Number(dateParts[1]) - 1,
        Number(dateParts[0])
    );
    if (filterLocalDateAtMidnight.getTime() === cellDate.getTime()) {
        return 0;
    }
    if (cellDate < filterLocalDateAtMidnight) {
        return -1;
    }
    if (cellDate > filterLocalDateAtMidnight) {
        return 1;
    }

}
1 Like

Thanks @jinnyzor! I’ll test this out as soon as I can. Then I’ll attempt to expand upon it and get the PersonFilter to work.

1 Like

Hi @robertf

Please note that the PR has not been merged or released yet. This will likely happen sometime next week. :crossed_fingers:

This will be available on dash-ag-grid>=2.4.0

1 Like

Hi @robertf

:tada: dash-ag-grid-2.4.0 is now available!

The example that @jinnyzor posted above now works. It shows how to use a custom component in the header to filter the grid. This example uses dcc.RadioItems

As a bonus, it also demonstrates how to filter the Date column using a date filter comparator function in JavaScript. You can use this instead of the d3.timeParse. More info in the dash docs date filters section.

ag-grid-custom-function

dag-docs

3 Likes

Thanks @jinnyzor and @AnnMarieW ! I’ll be working with this again soon and will report back if I am having issues adapting this to my use case.

1 Like

Hello @jinnyzor

I took some time to apply what you showed towards creating the PersonFilter from the docs. I am also wanting to add an Apply and Remove/Clear button, since it currently is actively filtering as the person types. I have things mostly working, except I am not able to figure out how to delay applying the filter or how to clear it. I tried a few things (like trying to save as a global variable) but nothing works. Also, I am not able to clear the textbox after typing in it (the 1st character always stays.)

Below is what I have so far.

app.py

from dash import Dash, html
import dash_ag_grid as dag
import pandas as pd

app = Dash(__name__)


df = pd.read_json('https://www.ag-grid.com/example-assets/olympic-winners.json', convert_dates=False)

rowData = df.to_dict('records')

columnDefs = [
    { 'field': 'athlete', 'minWidth': 150, 'filter': {'function': 'PersonFilter' } },
    { 'field': 'age', 'filter': 'agNumberColumnFilter' },
    { 'field': 'country', 'minWidth': 150 },
    # { 'field': 'year', 'filter': {'function': 'YearFilter' }},
    {
      'field': 'date',
      'minWidth': 130,
      'filter': 'agDateColumnFilter',
      'filterParams': {
        'comparator': {'function':'dateComparator'},
      },
    },
    { 'field': 'sport' },
    { 'field': 'gold', 'filter': 'agNumberColumnFilter' },
    { 'field': 'silver', 'filter': 'agNumberColumnFilter' },
    { 'field': 'bronze', 'filter': 'agNumberColumnFilter' },
    { 'field': 'total', 'filter': 'agNumberColumnFilter' },
]

defaultColDef = {
    'editable': True,
    'sortable': True,
    'flex': 1,
    'minWidth': 100,
    'filter': True,
    'resizable': True,
}


app.layout = html.Div(
    [
        dag.AgGrid(
            id="filter-component-example",
            columnDefs=columnDefs,
            rowData=rowData,
            defaultColDef=defaultColDef
        ),
    ]
)


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

dashAgGridFunctions.js

var dagfuncs = window.dashAgGridFunctions = window.dashAgGridFunctions || {};

const [useImperativeHandle, useRef, useState, useEffect, forwardRef, Component] = [React.useImperativeHandle, React.useRef, React.useState, React.useEffect, React.forwardRef, React.component]

dagfuncs.PersonFilter = forwardRef((props, ref) => {
    const [filterText, setFilterText] = useState(null);

  // expose AG Grid Filter Lifecycle callbacks
  useImperativeHandle(ref, () => {
    return {
      doesFilterPass(params) {
        const { api, colDef, column, columnApi, context } = props;
        const { node } = params;

        // make sure each word passes separately, ie search for firstname, lastname
        let passed = true;
        filterText
          .toLowerCase()
          .split(' ')
          .forEach((filterWord) => {
            const value = props.valueGetter({
              api,
              colDef,
              column,
              columnApi,
              context,
              data: node.data,
              getValue: (field) => node.data[field],
              node,
            });

            if (value.toString().toLowerCase().indexOf(filterWord) < 0) {
              passed = false;
            }
          });

        return passed;
      },

      isFilterActive() {
        return filterText != null && filterText !== '';
      },

      getModel() {
        if (!this.isFilterActive()) {
          return null;
        }

        return { value: filterText };
      },

      setModel(model) {
        setFilterText(model == null ? null : model.value);
      },
    };
  });

  setProps = (props) => {
    if (props.value) {
        setFilterText(props.value)
    }
    };

  useEffect(() => {
    props.filterChangedCallback();
  }, [filterText]);

  function buttonOnApply(){
    console.log('applied');
  };

  function buttonOnRemove(){
    console.log('filter removed');
  };

  return React.createElement(
    'div',
    {},
    [
        React.createElement(window.dash_core_components.Input, {type:'text', placeholder:'enter text', value:filterText, setProps}),
        React.createElement('button', {onClick:buttonOnApply}, 'Apply'),
        React.createElement('button', {onClick:buttonOnRemove}, 'Remove Filter'),
    ]
  )

});

Hello @robertf,

You were close, try this:

dagfuncs.PersonFilter = forwardRef((props, ref) => {
    const [filterText, setFilterText] = useState(null);

  // expose AG Grid Filter Lifecycle callbacks
  useImperativeHandle(ref, () => {
    return {
      doesFilterPass(params) {
        const { api, colDef, column, columnApi, context } = props;
        const { node } = params;

        // make sure each word passes separately, ie search for firstname, lastname
        let passed = true;
        filterText
          .toLowerCase()
          .split(' ')
          .forEach((filterWord) => {
            const value = props.valueGetter({
              api,
              colDef,
              column,
              columnApi,
              context,
              data: node.data,
              getValue: (field) => node.data[field],
              node,
            });

            if (value.toString().toLowerCase().indexOf(filterWord) < 0) {
              passed = false;
            }
          });

        return passed;
      },

      isFilterActive() {
        return filterText != null && filterText !== '';
      },

      getModel() {
        if (!this.isFilterActive()) {
          return null;
        }

        return { value: filterText };
      },

      setModel(model) {
        setFilterText(model == null ? null : model.value);
      },
    };
  });

  setProps = ({value}) => {
        setFilterText(value)
    };

  useEffect(() => {
    props.filterChangedCallback();
  }, [filterText]);

  function buttonOnApply(){
    console.log('applied');
  };

  function buttonOnRemove(){
    console.log('filter removed');
    setFilterText('')
  };

  return React.createElement(
    'div',
    {},
    [
        React.createElement(window.dash_core_components.Input, {type:'text', placeholder:'enter text', value:filterText, setProps, debounce: true}),
        React.createElement('button', {onClick:buttonOnApply}, 'Apply'),
        React.createElement('button', {onClick:buttonOnRemove}, 'Remove Filter'),
    ]
  )

});

This uses the input’s debounce prop to keep from rendering each time you type. Removing the filter button works.

Also, a slight modification to your setProps → setProps = ({value}) this means that we are only listening for the value object, and anytime this is updated, we would apply it, including blank strings (this registers false when testing for values).

1 Like