Skip to content
349 changes: 349 additions & 0 deletions docs/blog/posit-post/index.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,349 @@
---
title: "How To Use gt-extras to Make Beautiful Tables"
html-table-processing: none
author: Jules Walzer-Goldfeld
date: 2025-07-23
freeze: true
jupyter: python3
toc-expand: 2
---

![](../../assets/2011-nfl-season.png){width=800 fig-align="center"}

## What is gt-extras?

You've just finished analyzing your data, crafted the perfect summary table, but now what? The numbers tell an important story, but they're lost in a sea of black text on white background.

That's where [**gt-extras**](https://posit-dev.github.io/gt-extras/) comes in. While [**Great Tables**](https://posit-dev.github.io/great-tables/) gives you the foundation for beautiful, publication-ready tables in Python, **gt-extras** comes with specialized plotting functions, color schemes, themes, and visual enhancements that take your tables one step further.

In this post, we'll walk through building a comprehensive NFL season analysis that showcases the power of combining multiple **gt-extras** functions. You'll learn how to:

- Transform raw game outcomes into visual sparklines
- Use color coding to highlight performance patterns
- Create comparative visualizations for related metrics
- Apply professional themes for publication-ready results
- Combine multiple tables into space-efficient layouts

Here’s what we’ll build:

```{python}
# | code-fold: True
# | code-summary: Show the code
# | output: False
import polars as pl
from great_tables import GT, md
import gt_extras as gte

nfl_df = pl.read_json("../../examples/nfl-season/nfl_2011_stats.json")

def make_gt(df) -> GT:
return (
GT(df, rowname_col="Team", groupname_col="team_division")
.tab_header(
title="2011 NFL Season at a Glance",
subtitle="Super Bowl XLVI: New York Giants def. New England Patriots",
)
.tab_source_note(
md(
'<span style="float: right;">Source: [Lee Sharpe, nflverse](https://github.com/nflverse/nfldata)</span>'
)
)
.tab_stubhead(label="Team")
.tab_spanner(label="Scoring", columns=["Avg PF", "Avg PA", "Point Diff"])
.tab_spanner(label="Season Trends", columns=["Games", "Streak"])
.fmt_number(columns="Point Diff", decimals=1)
.fmt_image("team_logo_espn")
.cols_hide(["Wins", "team_conf", "team_name"])
.cols_align(align="center", columns=["Avg PF", "Avg PA", "Games"])
.cols_move("team_logo_espn", after="Team")
.cols_label({"team_logo_espn": ""})
.pipe(
gte.gt_plt_winloss,
column="Games",
win_color="blue",
loss_color="orange",
tie_color="gray",
height=40,
width=120,
)
.pipe(
gte.gt_fa_rank_change,
column="Streak",
icon_type="turn",
color_up="blue",
color_down="orange",
size=14,
)
.pipe(
gte.gt_plt_dumbbell,
col1="Avg PF",
col2="Avg PA",
col1_color="green",
col2_color="red",
width=300,
height=40,
num_decimals=0,
label="Points For vs Points Against",
font_size=12,
)
.pipe(
gte.gt_color_box,
columns="Point Diff",
palette=["green", "yellow", "red"],
domain=[14, -14]
)
.pipe(gte.gt_highlight_cols, columns="Team", fill="lightgray")
.pipe(gte.gt_highlight_rows, rows="NYG", fill="gold", alpha=0.3)
.pipe(gte.gt_highlight_rows, rows="NE", fill="silver", alpha=0.3)
.pipe(gte.gt_add_divider, columns="Point Diff", color="darkblue", weight=3)
.pipe(gte.gt_theme_538)
)


gt1 = make_gt(nfl_df.head(16))
gt2 = make_gt(nfl_df.slice(16, 16))

gte.gt_two_column_layout(gt1, gt2, table_header_from=1)
```

![](../../assets/2011-nfl-season.png){width=600 fig-align="center"}

## Setup

First we load our dataset. I've chosen [NFL season data](https://github.com/nflverse/nfldata) from 2011 (courtesy of Lee Sharpe) because it's ripe with opportunity to visualize. I hope you'll find this is true for all datasets as long as you've got a dash of creativity (or [**gt-extras**](https://posit-dev.github.io/gt-extras/))!

Data cleaning and setup aren’t the focus of this post, so I’ve left the code in a notebook for you to inspect if you wish. In the notebook, we join two datasets, one that gives us 2011 football statistics and the other team metadata.

<a href="https://github.com/posit-dev/gt-extras/blob/main/docs/examples/nfl-season/index.ipynb" target="_blank">View notebook ⬀</a>

```{python}
# | warning: False
import polars as pl
import requests
import io

url = "https://raw.githubusercontent.com/posit-dev/gt-extras/main/docs/examples/nfl-season/nfl_2011_stats.json"
response = requests.get(url)
nfl_df = pl.read_json(io.StringIO(response.text))

nfl_df.glimpse()
```

## Making a GT

Let's start without any styling and see what our starting point is. You'll notice each row is rather tall, so for now I'm going to take the first two rows. Eventually we'll go back to the original dataframe.

```{python}
from great_tables import GT, md
import gt_extras as gte

df_head = nfl_df.head(2)

gt = GT(df_head, rowname_col="Team", groupname_col="team_division")

gt
```

Woah, that is unwieldy! That is partially my doing, because some of the setup was designed with **gt-extras** in mind. You'll notice the list of 1's and 0's (and possibly even some 0.5s when the rare tie occurred).

[**Great Tables**](https://posit-dev.github.io/great-tables/) on its own can do a lot to help get this table into a prettier state. I'm going to apply several `GT` style methods, most visibly `GT.tab_spanner()`, `GT.fmt_image()` and some column hiding, moving, and naming.

```{python}
# | code-fold: True
# | code-summary: Show the code for GT styling

df_head = nfl_df.head(6)

def make_initial_gt(df) -> GT:
return (
GT(df, rowname_col="Team", groupname_col="team_division")
.tab_header(
title="2011 NFL Season at a Glance",
)
.tab_source_note(
md(
'<span style="float: right;">Source: [Lee Sharpe, nflverse](https://github.com/nflverse/nfldata)</span>'
)
)
.tab_stubhead(label="Team")
.tab_spanner(label="Scoring", columns=["Avg PF", "Avg PA", "Point Diff"])
.tab_spanner(label="Season Trends", columns=["Games", "Streak"])
.fmt_number(columns="Point Diff", decimals=1)
.fmt_image("team_logo_espn")
.cols_hide(["Wins", "team_conf", "team_name"])
.cols_align(align="center", columns=["Avg PF", "Avg PA", "Games"])
.cols_move("team_logo_espn", after="Team")
.cols_label({"team_logo_espn": ""})
)


gt = make_initial_gt(df_head)
gt
```

After some `GT` styling, the table starts to look a little bit nicer. Let's see what **gt-extras** can do!

## GT Extras

::: {.callout-note collapse="false"}
I'm going to isolate each **gt-extras** component into its own function, for the sake of the two-column finale. For now, it's sufficient to call each tool independently.


- Option 1: Call the function directly
- `gte.gt_plt_winloss(gt, column="Games")`

- Option 2: Call [`GT.pipe()`](https://posit-dev.github.io/great-tables/reference/GT.pipe) which allows for method chaining
- `gt.pipe(gte.gt_plt_winloss, column="Games")`


There isn't necessarily a "correct" way to apply the **gt-extras** functionality, but I prefer option 2. Towards the end, the [theming code block](#theming-and-more) is a good example of why I like `GT.pipe()`.
:::


### Win-Loss Plotting

```{python}
def add_winloss(gt: GT) -> GT:
return gt.pipe(
gte.gt_plt_winloss,
column="Games",
win_color="blue",
loss_color="orange",
tie_color="gray",
height=40,
width=120,
)


gt = add_winloss(gt)
gte.gt_highlight_cols(gt, "Games", include_column_labels=True)
```

The `gt_plt_winloss()` function is one of my favorite sparkline plotting functions. Each bar represents individual game outcomes, which is why we needed the data in each cell to be in the form of a list.

### Change Icons

```{python}
def add_change_icons(gt: GT) -> GT:
return gt.pipe(
gte.gt_fa_rank_change,
column="Streak",
icon_type="turn",
color_up="blue",
color_down="orange",
size=14,
)

gt = add_change_icons(gt)
gte.gt_highlight_cols(gt, "Streak", include_column_labels=True)
```

The [`gt_fa_rank_change()`](https://posit-dev.github.io/gt-extras/reference/gt_fa_rank_change) function has a range of good applications. I'm using it here to indicate a winning or losing streak at the end of the season, but another great use case would to easily visualize the direction of a percentage change.


### Dumbbell Plotting

```{python}
def add_dumbbell(gt: GT) -> GT:
return gt.pipe(
gte.gt_plt_dumbbell,
col1="Avg PF",
col2="Avg PA",
col1_color="green",
col2_color="red",
width=300,
height=40,
num_decimals=0,
label="Points For vs Points Against",
font_size=12,
)

gt = add_dumbbell(gt)
gte.gt_highlight_cols(gt, "Avg PF", include_column_labels=True)
```

The [`gt_plt_dumbbell()`](https://posit-dev.github.io/gt-extras/reference/gt_plt_dumbbell) function creates a great comparative visualization for two related metrics. Here, it beautifully shows the relationship between points scored (green) and points allowed (red) for each team. You can instantly see which teams have strong offenses, strong defenses, or both!

### Color Box

```{python}
def add_color_box(gt: GT) -> GT:
return gt.pipe(
gte.gt_color_box,
columns="Point Diff",
palette=["green", "yellow", "red"],
domain=[14, -14],
)

gt = add_color_box(gt)

gte.gt_highlight_cols(gt, "Point Diff", include_column_labels=True)

```

The [`gt_color_box()`](https://posit-dev.github.io/gt-extras/reference/gt_color_box) function provides an immediate visual indicator of performance through color coding. The gradient from green (positive) to red (negative) makes it easy to spot the strongest and weakest teams at a glance without having to parse the dumbbells.

At this point, you may have noticed just how much room we have to customize when calling the **gt-extras** functions. Aspects such as coloring, width, height, font size, among many others, are commonly available no matter which graphic you are adding to your table. For example, I've taken advantage of the coloring to emphasize the grouping of the two sections of the table.

### Theming and More

```{python}
def add_extra_styles(gt: GT) -> GT:
return (
gt.tab_header(
title="2011 NFL Season at a Glance",
subtitle="Super Bowl XLVI: New York Giants def. New England Patriots",
)
.pipe(gte.gt_highlight_cols, columns="Team", fill="lightgray")
.pipe(gte.gt_highlight_rows, rows="NYG", fill="gold", alpha=0.3)
.pipe(gte.gt_highlight_rows, rows="NE", fill="silver", alpha=0.3)
.pipe(gte.gt_add_divider, columns="Point Diff", color="darkblue", weight=3)
.pipe(gte.gt_theme_538)
)


add_extra_styles(gt)
```

For the first time, we're seeing multiple **gt-extras** styling functions working together. We're highlighting the Super Bowl teams (Giants in gold, Patriots in silver), adding visual dividers for better organization, and applying the clean FiveThirtyEight theme. The result is a polished, publication-ready table that tells the story of the 2011 NFL season.

### Two-Column Layout

One last detail that bothers me is that we can't fit all 32 teams into one table. Fortunately, we can take advantage of the [`gt_two_column_layout()`](https://posit-dev.github.io/gt-extras/reference/gt_two_column_layout) function. This allows us to split into two tables of half-height each, which is a much nicer shape to view on a screen or in an article.

```{python}
def make_gt(df) -> GT:
gt = make_initial_gt(df)
gt = add_winloss(gt)
gt = add_change_icons(gt)
gt = add_dumbbell(gt)
gt = add_color_box(gt)
gt = add_extra_styles(gt)
return gt


make_gt(nfl_df.head(20))
```

That was barely more than the first half of the table. It's already a bit tall, and imagine displaying all 32 teams...

Instead, let's use [`gt_two_column_layout()`](https://posit-dev.github.io/gt-extras/reference/gt_two_column_layout) for the combined final table!

```{python}
# | output: False
gt1 = make_gt(nfl_df.head(16))
gt2 = make_gt(nfl_df.slice(16, 16))

gte.gt_two_column_layout(gt1, gt2, table_header_from=1)
```

![](../../assets/2011-nfl-season.png){fig-align="center"}

## Wrapping Up

With just a few lines of code, [**gt-extras**](https://posit-dev.github.io/gt-extras/) transforms ordinary tables into compelling visual summaries. Whatever your dataset, these tools help you tell a clearer, more engaging story. Try combining different components and themes to fit your own data and audience!

Let us know how you're using [**gt-extras**](https://posit-dev.github.io/gt-extras/), or help us improve it:

- [GitHub Discussions](https://github.com/posit-dev/gt-extras/discussions)
- [GitHub Issues](https://github.com/posit-dev/gt-extras/issues)
Loading