The world’s leading publication for data science, AI, and ML professionals.

How to Create Well-Styled Streamlit Dataframes, Part 2: using AgGrid

The pandas Styler is cool. But AgGrid is way cooler. Make your Streamlit dataframes interactive and stunning.

In my previous post, we covered how to create pretty styled dataframes using the pandas Styler object. In this post, I want to tell you about a second option: Streamlit AgGrid. I will share how to build dataframes like the one displayed above. After reading this post you will learn:

  1. The key components inside AgGrid. These are gridOptions() , configure_column() , configure_default_column(), and configure_side_bar().
  2. The main options to enable filtering and aggregating tables using the directly the UI. No more building ad-hoc queries for simple transformations!
  3. Making the dataframe prettier with Javascript functions. You will be able to copy paste them in your code if you want. Or check my Git repo.

Disclaimer 1: I have no affiliation or partnership with AgGrid. I just find a lot of value in the open-source product. AgGrid does have a paid tiered product, but the blog will only use the free components of AgGrid.

Disclaimer 2: All images and GIFs are authored by myself unless specified otherwise.


What is AgGrid

AgGrid is a powerful and customizable data grid component that you can use in web applications to display and manipulate large amounts of data in a table format. The best analogy I find is that it is the web-app version of Excel. Think of it as a supercharged table that not only shows data but also allows users to interact with it in many ways, like editing cells, aggregating & pivoting, or even integrating with charts. It’s actually used in a lot of enterprises.

Now, AgGrid is built using Javascript 😱😱😱 . So, yes, there is a some learning to do. But, you don’t need to be a Javascript expert! And that is thanks to Pablo Fonseca, who migrated the functionality of the Ja[vaSript AgGrid component 1] to Streamlit applications.


Understanding the basic components of AgGrid

I started my AgGrid journey reading Ahmed Besbes‘ article: 7 Reasons Why You Should Use the Streamlit AgGrid Component [2]. It was a great starting point to understand why I wanted to use AgGrid. However, I was missing a comprehensive tutorial to get my head around how to use AgGrid. In this section I will try to cover the concepts that made the AgGrid puzzle click in my head.

Basic AgGrid: you just need a dataframe and gridOptions object

The AgGrid method has a truck load of options… check the screenshot below.

Image from documentation and edited by author
Image from documentation and edited by author

In reality, there are only 2 parameters you need: dataframe and gridOptions (highlighted in yellow). That’s it. Just pass the dataframe and AgGrid will come out of the box with some cool features.

standard_AgGrid = AgGrid(
  df, 
  gridOptions=GridOptionsBuilder.from_dataframe(df).build()
)

# You can see how you don't even need the st.dataframe() Streamlit method.
# Just by calling AgGrid, the rendering will automatically happen.

AgGrid will, by default, allow you to sort, filter and move columns around. That is a nice addition compared to the default Streamlit st.dataframe() method. Below are a series of GIFs showing you what the default AgGrid offers you in terms of functionality.

This is how to select and deselect columns
This is how to select and deselect columns
This is how you re-order columns
This is how you re-order columns
This is how you filter data
This is how you filter data

valueGetter vs valueFormatter

To begin adding some nice formatting to our dataframe, we first need to understand what valueGetter and valueFormatter do. I will explain the basic theory and show you examples later.

  • valueGetter is used to retrieve or compute the value that should be displayed in a cell. It can be used to manipulate or combine data from different columns or perform calculations. But no formatting is involved.
  • valueFormatter is used to format the value in a cell for display purposes. It doesn’t change the actual value, just how it looks when displayed.
// Imagine you have a table where you store product prices, 
// but you want to display the price including tax (10%)
// You can use valueGetter to calculate the price including tax:

valueGetter: function(params) {
    return params.data.price * 1.10;  // Add 10% tax to the original price
}

// Instead of having 4 decimal points, you might want to DISPLAY only
// 1 decimal point and add a currency symbol. 
// This what valueFormatter is for.

valueFormatter: function(params) {
    return '$' + params.value.toFixed(1);  // Display value as currency
}

But how to inject this Javascript code into our Python & Streamlit code?

Use JsCode provided inside the st_aggrid package. Check the code example below:

from st_aggrid import AgGrid, GridOptionsBuilder, JsCode

# Define JsCode for valueGetter (convert cents to dollars)
value_getter = JsCode('''
function(params) {
    return params.data.price_cents / 100;  // Convert cents to dollars
}
''')

# Define JsCode for valueFormatter (format as currency)
value_formatter = JsCode('''
function(params) {
    return '$' + params.value.toFixed(2);  // Format the value as currency
}
''')

You will learn how to use these value_getter and value_formatter when we useconfigure_column().


configure_column() is where the formatting magic happens

The default AgGrid view had some nice out-of-the-box features (sorting, filtering, column selection and dragging columns). However, the output is a bit dull… 🥱

To make it nicer we need to combine a value_getter and value_formatter Javascript set of functions and use them in configure_column()

Unfortunately, the documentation is not very detailed, but I will show you things are done

Image from official documentation. Little documentation for the configure_column. Just a **other_column_properties argument.
Image from official documentation. Little documentation for the configure_column. Just a **other_column_properties argument.

Cleaning a column header

If you simply want to display the column name differently, there is no need to rename the columns in your dataframe. You can pass it directly to the configure_column() through the header_name parameter

from st_aggrid import AgGrid, GridOptionsBuilder

grid_builder = GridOptionsBuilder.from_dataframe(df)

# Renaming "Period_1" to "Period 1"
grid_builder.configure_column('Period_1', header_name='Period 1')

# Build grid options
gridOptions = grid_builder.build()

grid_response = AgGrid(df,
                       gridOptions=gridOptions,
                       )
Renaming "Period_1" to "Period 1"
Renaming "Period_1" to "Period 1"

Currency formating

Do you want to add a currency sign before a number, separate the thousands and force a certain number of decimal points? Let’s make our first use of value_getter and value_formatter and JsCode .

The code below does the following:

  1. Define which value will the cells use. The currency_getter will use the values in the field we pass to it.
  2. Define how you want to display the values in the cells. The currency_formatter will return a currency_symbol + formattedNumber .
  3. Passing variables into our JsCode formula. Using the cellRendererParams . For example, how will currency_formatter know which symbol and decimal points to use? cellRendererParams allows us to pass variables into our Javascript function.
  4. Enable Streamlit to render custom Javascript code. Use allow_unsafe_jscode=True in AgGrid().
currency_formatter = JsCode("""
function(params) {
    if (params.value == null || params.value === undefined) {
        return '';
    }
    var decimalPoints = params.column.colDef.cellRendererParams.decimalPoints || 0;
    var currencySymbol = params.column.colDef.cellRendererParams.currencySymbol || '€';
    var value = params.value;

    // Format the number with thousand separators and decimal points
    var formattedNumber = value.toLocaleString('en-US', {
        minimumFractionDigits: decimalPoints,
        maximumFractionDigits: decimalPoints
    });

    return currencySymbol + formattedNumber;
}
""")

currency_getter = JsCode("""
function(params) {
    return params.data[params.colDef.field];
}
""")

grid_builder = GridOptionsBuilder.from_dataframe(df)

grid_builder.configure_column(
  'Period_1',
  header_name='Period 1',
  type=['numericColumn', 'numberColumnFilter', 'customNumericFormat'],
  valueGetter=currency_getter,
  valueFormatter=currency_formatter,
  cellRendererParams={
      'decimalPoints': 0,
      'currencySymbol': '€',
  }
)

# Build grid options
gridOptions = grid_builder.build()

grid_response = AgGrid(df,
                       gridOptions=gridOptions,
                       allow_unsafe_jscode=True,
                       )

Using this for _Period1, _Period2 and Difference, we get the following

Currency formatting
Currency formatting

Percentage change formatting

If you have understood the code snippet above, then the next will be easy to follow.

percentage_formatter = JsCode("""
function(params) {
    if (params.value == null) {
        return '';
    }
    var decimalPoints = params.column.colDef.cellRendererParams.decimalPoints || 2;
    return (params.value * 100).toFixed(decimalPoints) + '%';
}
""")

percentage_getter = JsCode("""
function(params) {
    return params.data[params.colDef.field];
}
""")

# jumping directly to configure_column() 
grid_builder.configure_column(
  'Percentage Change',
  header_name='Percentage Change (%)',
  type=['numericColumn', 'numberColumnFilter', 'customNumericFormat'],
  valueGetter=percentage_getter,
  valueFormatter=percentage_formatter,
  cellRendererParams={'decimalPoints': 1,},
  )
Percentage change formatting
Percentage change formatting

Highlighting cell background

How can we recreate those green and red gradients we showed when using the Styler object?

  1. You should know by now that we first need to create a JsCode function. This one is long, so best to check my repo.
  2. But what we haven’t seen is how to pass this UI custom formatting. This is done through the cellStyle parameter in configure_column().
# Check my git repo for the function details
cellStyle = JsCode(
  """function(params) {...}"""
)

grid_builder.configure_column(
  'Percentage Change',
  header_name='Percentage Change (%)',
  type=['numericColumn', 'numberColumnFilter', 'customNumericFormat'],
  valueGetter=percentage_getter,
  valueFormatter=percentage_formatter,
  cellRendererParams={'decimalPoints': 1,
                      'minValue': df['Percentage Change'].min(),
                      'maxValue': df['Percentage Change'].max()
                      },
  cellStyle=cellStyle, # here is where we format the UI appearance of a cell
)
Highlighting cell background
Highlighting cell background

Adding emojis

Because emojis are only extra characters in a cell, we don’t need value_getter , only value_formatter . Remember, you only need value_getter if you need the value_formatter to perform an operation to the cell (multiply by 100, rounding, etc). In this case, no operation is required for emojis. The functions are simple.

# Define JsCode for emoji formatting
medalFormatter = JsCode("""
function(params) {
    if (params.value == null || params.value === undefined) {
        return '';
    }
    var val = params.value;
    if (val === 1) {
        return val + ' 🥇';
    } else if (val === 2) {
        return val + ' 🥈';
    } else if (val === 3) {
        return val + ' 🥉';
    } else {
        return val;
    }
}
""")

# Define JsCode for country formatting
countryFormatter = JsCode("""
function(params) {
    if (params.value == null || params.value === undefined) {
        return '';
    }
    var countryEmojis = {
        "US": "🇺🇸 ",
        "IN": "🇮🇳 ",
        "BR": "🇧🇷 ",
        "ES": "🇪🇸 ",
        "AR": "🇦🇷 ",
        "IT": "🇮🇹 ",
        "EG": "🇪🇬 "
        // Add more countries as needed
    };
    var countryCode = params.value;
    var emoji = countryEmojis[countryCode] || '';
    return emoji + ' ' + countryCode;
}
""")

# jumping directly to configure_column()
grid_builder.configure_column(
  'Percentage Change rank',
  header_name='Percentage Change rank',
  type=['numericColumn', 'numberColumnFilter', 'customNumericFormat'],
  valueFormatter=medalFormatter,
)

grid_builder.configure_column(
  'Country',
  header_name='Country',
  type=['textColumn', 'stringColumnFilter'],
  valueFormatter=countryFormatter,
)
Adding emojis
Adding emojis

Adding bars

In excel, we have seen plenty of times the formatting of a cell where we have bars in the background of a cell. We can achieve the same in AgGrid _(similar to highlighting cell background section, check the code in my repo. It’s too long to paste here)_.

Adding bars to Period 1 and Difference. See that when negative numbers exist (Difference column), the bars are rendered using the middle of the cell as the anchor point.
Adding bars to Period 1 and Difference. See that when negative numbers exist (Difference column), the bars are rendered using the middle of the cell as the anchor point.

Changing the overall table UI display

Up until this point, we have definitely made the cells of the dataframe prettier. AgGrid also allows controlling features related to the whole table. For example:

  • Manually set the overall height and/or width of the table
  • Changing the theme of the table

Let’s see some of these examples:

Manually set the overall height of the table

By default, AgGrid will add white space to the end of the table. Check the screenshot below:

To control the height of table, it is super easy; just use the height parameter. In the snippet below I have decided to use 60px per row, which still adds that whitespace, but change this to your convenience.

grid_response = AgGrid(
  df,
  gridOptions=gridOptions,
  allow_unsafe_jscode=True,
  height=min(2000, (len(df)) * 60),  # 60px per row or 2000px
)

Changing the theme of the table

In Streamlit AgGrid you only have 4 options to change the theme: streamlit, balham, alpine and material. My default one is balham because it is the denser table. But below you can see how wide the columns and rows are with each theme.

grid_response = AgGrid(
  df,
  gridOptions=gridOptions,
  allow_unsafe_jscode=True,
  height=min(2000, (len(df)) * 60),  # 60px per row or 2000px
  fit_columns_on_grid_load=False,
  theme='balham', # options: streamlit, alpine, balham, material
)
Theme: streamlit
Theme: streamlit
Theme alpine
Theme alpine
Theme material
Theme material

Adding pagination

Pagination is a must-have if you are dealing with big datasets. The data used throughout this blog post is only 7 rows long, but here is what is would look like if we configured pagination to create pages for every 3 rows.

# Enable pagination
grid_builder.configure_pagination(
  paginationAutoPageSize=False, 
  paginationPageSize=3
)
Pagination at 3 rows
Pagination at 3 rows

Passing the AgGrid object to use later

Remember how cool was the filtering behaviour that AgGrid enables? Well, what if we could pass that filtered dataframe to use later? To do this, we need to enable are 2 parameters: data_return_mode and update_mode . We also need to create a new dataframe with the the grid response data.

grid_response = AgGrid(
  df,
  gridOptions=gridOptions,
  allow_unsafe_jscode=True,
  height=min(2000, (len(df)) * 60),  # 60px per row or 2000px
  fit_columns_on_grid_load=False,
  theme='balham', # options: streamlit, alpine, balham, material
  data_return_mode='FILTERED_AND_SORTED', # there are other options, read the docs
  update_mode='MODEL_CHANGED' # there are other options, read the docs
)

# https://streamlit-aggrid.readthedocs.io/en/docs/AgGrid.html
# Create filtered dataframe
filtered_df = pd.DataFrame(grid_response['data'])

return grid_response, filtered_df

Check the behaviour in Streamlit.

Example on how to pass the filtered dataframe (above) as a new output (below). The GIF is slightly long, give it 30secs or so to fully render.
Example on how to pass the filtered dataframe (above) as a new output (below). The GIF is slightly long, give it 30secs or so to fully render.

Allowing aggregation in the UI

Whenever I have create a Streamlit app, I normally have to manually create functionalities to aggregate. For example, adding a "aggregate by" st.selectbox() and link it to an aggregation function. AgGrid offers the possibility to not have to code up these widgets and functions. Here is how you do it:

  1. Force the aggregation configuration in the configure_default_column()
  2. For each column, specify which aggregation function you want to perform.
grid_builder = GridOptionsBuilder.from_dataframe(df)

grid_builder.configure_side_bar()
grid_builder.configure_default_column(
  filter=True, 
  groupable=True, 
  value=True, 
  enableRowGroup=True, 
  aggFunc="sum" # sum would be applied to all columns
) 

grid_builder.configure_column(
  'Month',
  header_name='Month',
  type=['numericColumn', 'numberColumnFilter'],
  aggFunc=None, # If you dont want month to be passed to the "sum" aggregation, use None
)

grid_builder.configure_column(
  'Percentage Change',
  header_name='Percentage Change (%)',
  type=['numericColumn', 'numberColumnFilter', 'customNumericFormat'],
  aggFunc="avg", # you can specify the average for % change
  valueGetter=percentage_getter,
  valueFormatter=percentage_formatter,
  cellRendererParams={'decimalPoints': 1,
                      },
)

The nice thing about how AgGrid displays the aggregation is that it also allows you to drill down on the "group by column".

Example of running aggregations on the fly through the UI.
Example of running aggregations on the fly through the UI.

Summary

If you have reached all way down here, you have my respect 🙇 . I know this is a long post, but I wanted to write useful detailed examples. I haven’t found a tutorial elsewhere, and hopefully this is something similar to one.

To summarise what we have covered:

  1. The basic components of AgGrid.
  2. Functions to format the cells of a table.
  3. How to format the whole dataframe.
  4. How to pass the dataframe as an output object
  5. How to aggregate using the UI.

Where can you find the code?

In my repo and the live Streamlit app:

Acknowledgements

Further reading

Thanks for reading the article! If you are interested in more of my written content, here is an article capturing all of my other blogs posts organised by themes: Data Science team and project management, Data storytelling, Marketing & bidding science and Machine Learning & modelling.

All my written articles in one place

Stay tuned!

If you want to get notified when I release new written content, feel free to follow me on Medium or subscribe to my Substack newsletter. In addition, I would be very happy to chat on Linkedin!

Get notified with my latest written content about Data Science!

Jose’s Substack | Jose Parreño Garcia | Substack


Related Articles