Three Approaches to Styling the Table Body in Great Tables

python
polars
gt
Author

Jerry Wu

Published

January 24, 2025

This post demonstrates three approaches to styling the table body:

Let’s dive in.

Preparations

We’ll use the built-in dataset gtcars to create a Polars DataFrame. First, we’ll select the columns mfr, drivetrain, year, and hp to create a small pivoted table named df_mini. Then, we’ll pass df_mini to the GT object and use GT.tab_stub(), setting drivetrain as rowname_col= and mfr as groupname_col= to create the table gt, as shown below:

Code
import polars as pl
from great_tables import GT, loc, style
from great_tables.data import gtcars
from polars import selectors as cs

year_cols = ["2014", "2015", "2016", "2017"]
df_mini = (
    pl.from_pandas(gtcars)
    .filter(pl.col("mfr").is_in(["Ferrari", "Lamborghini", "BMW"]))
    .sort("drivetrain")
    .pivot(on="year", index=["mfr", "drivetrain"], values="hp", aggregate_function="mean")
    .select(["mfr", "drivetrain", *year_cols])
)

gt = GT(df_mini).tab_stub(rowname_col="drivetrain", groupname_col="mfr").opt_stylize()
gt
2014 2015 2016 2017
Ferrari
awd None 652.0 None 680.0
rwd 562.0 678.4 661.0 None
Lamborghini
awd None 700.0 None None
rwd 550.0 610.0 None None
BMW
awd None None 357.0 None
rwd None None 465.0 None

The numbers in the cells represent the average horsepower for each combination of mfr and drivetrain for a specific year.

In the following section, we’ll demonstrate three different ways to highlight the cell text in red if the average horsepower exceeds 650.

Using a For-Loop: Repeatedly Call GT.tab_style() for Each Column

The most intuitive way is to call GT.tab_style() for each column. Here’s how:

1gt1 = gt
for col in year_cols:
    gt1 = gt1.tab_style(
        style=style.text(color="red"),
        locations=loc.body(columns=col, rows=pl.col(col).gt(650))
    )
gt1
1
Since we want to keep gt intact for later use, we will modify gt1 in this approach instead.
2014 2015 2016 2017
Ferrari
awd None 652.0 None 680.0
rwd 562.0 678.4 661.0 None
Lamborghini
awd None 700.0 None None
rwd 550.0 610.0 None None
BMW
awd None None 357.0 None
rwd None None 465.0 None

Utilizing the locations= Parameter in GT.tab_style(): Pass a List of loc.body() Objects

A more concise method is to pass a list of loc.body() objects to the locations= parameter in GT.tab_style(), as shown below:

(
    gt.tab_style(
        style=style.text(color="red"),
        locations=[
            loc.body(columns=col, rows=pl.col(col).gt(650))
            for col in year_cols
        ],
    )
)
2014 2015 2016 2017
Ferrari
awd None 652.0 None 680.0
rwd 562.0 678.4 661.0 None
Lamborghini
awd None 700.0 None None
rwd 550.0 610.0 None None
BMW
awd None None 357.0 None
rwd None None 465.0 None

Leveraging the mask= Parameter in loc.body(): Use Polars Expressions for Streamlined Styling

The most modern approach (0.16.0) is to pass a Polars expression to the mask= parameter in loc.body(), as illustrated here:

(
    gt.tab_style(
        style=style.text(color="red"),
        locations=loc.body(mask=cs.numeric().gt(650))
    )
)
2014 2015 2016 2017
Ferrari
awd None 652.0 None 680.0
rwd 562.0 678.4 661.0 None
Lamborghini
awd None 700.0 None None
rwd 550.0 610.0 None None
BMW
awd None None 357.0 None
rwd None None 465.0 None

In this example, loc.body() is smart enough to automatically target the rows where the cell value exceeds 650 for each numerical column. In general, you can think of mask= as a syntactic sugar that Great Tables provides to save you from having to manually loop through the columns.

Final Words

This post summarizes three approaches to styling the table body. Among them, the mask= parameter in loc.body() is definitely my favorite, inspired by #389 and implemented by me.

Special thanks to @rich-iannone and @machow for their invaluable suggestions during development. Any remaining bugs are entirely on me.

Disclaimer

This post was drafted by me, with AI assistance to refine the content.