I’ll be honest: I didn’t know what to call this function. I still
don’t think that gt_border_bars
is a particularly
good name. That said, this function offers so much
versatility for your table designs.
In essence, it places colored bars at the top or bottom of your table. You’re able to stack as many bars as you’d like, or you could pick just one color and include an image or text. The inspiration for this function came from two different places: a) it opens the possibility of more professional table footers with company logos and b) you can include team logos or player headshots at the top of table recaps.
In this vignette, we’re going to explore three distinct uses of the
gt_border_bars
family.
Team Reports
I’m a basketball guy at heart. A lot of my visualization work
concerns college basketball – so let’s create a 2024 metric recap for
Duke, my alma mater, using gt_border_bars_top
.
For data, we’re going to use my {cbbdata} package. The purpose of this vignette is to show off some quick uses of the function, so we’re going to skip most discussion around the processing of this data.
Let’s define the metrics we care about and whether a higher value is better. We’re going to plot national percentiles on a location split (home vs. away).
stats_to_consider <- c(
"team", "barthag", "adj_o", "adj_d", "adj_t", "efg", "def_efg", "ftr", "def_ftr",
"oreb_rate", "dreb_rate", "tov_rate", "def_tov_rate",
"two_pt_pct", "three_pt_pct", "ft_pct", "def_two_pt_pct",
"def_three_pt_pct", "def_ft_pct"
)
higher_better <- c(
"barthag", "adj_o", "adj_t", "efg", "ftr", "oreb_rate", "def_tov_rate",
"two_pt_pct", "three_pt_pct", "ft_pct"
)
Let’s get the data:
home_splits <- cbd_torvik_team_factors(venue = "home", year = 2024) %>%
select(all_of(stats_to_consider))
away_splits <- cbd_torvik_team_factors(venue = "away", year = 2024) %>%
select(all_of(stats_to_consider))
Here’s a quick function to take that data and calculate percentiles
using dplyr
. We’re going to pivot the data to a long format
for later use.
calculate_percentiles <- function(data, location) {
data %>%
mutate(across(all_of(higher_better), ~percent_rank(.x)),
across(setdiff(stats_to_consider, c("team", higher_better)), ~percent_rank(-.x))) %>%
pivot_longer(-team, names_to = "stat", values_to = "percentile") %>%
mutate(location = location)
}
Now, let’s calculate those percentiles on each split and combine the data. We need to pivot the final result to a wide format so that we can plot home and away columns.
home_percentiles <- calculate_percentiles(home_splits, "home")
away_percentiles <- calculate_percentiles(away_splits, "away")
final_data <- bind_rows(home_percentiles, away_percentiles) %>%
pivot_wider(names_from = location, values_from = percentile)
For final processing, we need to do two more things: a) add the general “group” of each stat and b) rename each stat to something more readable:
dict <- data.frame(
stat = unique(final_data$stat),
new_name = c("Barthag", "Adj. Off", "Adj. Def", "Adj. Tempo", "Eff. FG%", "Eff. FG%",
"FTA per FGA", "FTA per FGA", "Off. Rebound Rate", "Def. Rebound Rate",
"Turnover Rate", "Turnover Rate", "2FG%", "3FG%", "FT%", "2FG%", "3FG%", "FT%")
) %>% deframe()
final_data <- final_data %>%
filter(team == "Duke") %>%
mutate(
stat_group = case_when(
stat %in% c("barthag", "adj_o", "adj_d", "adj_t") ~ "Metrics",
stat %in% c("efg", "ftr", "oreb_rate", "tov_rate") ~ "Offensive Four Factors",
stat %in% c("def_efg", "def_ftr", "dreb_rate", "def_tov_rate") ~ "Defensive Four Factors",
stat %in% c("two_pt_pct", "three_pt_pct", "ft_pct") ~ "Offensive Shooting",
.default = "Defensive Shooting"
),
stat = dict[stat]
)
Awesome! Now we have our data. Let’s throw it over to
gt
. To create row groups in gt
, remember to
pass in your data grouped by that category. For this table, we
are using several functions from gtUtils
:
-
gt_theme_savant
theme for a clean look -
gt_color_pills
to fill our two percentile columns. Notice that we are formatting each as a percent and trimming to one digit. We also set our domain, [0, 1], and pass through a palette. -
gt_538_caption
to include some more information at the bottom of the table.
All said, gt_border_bars_top
is the function that we
really care about. To keep with our theming, we are using a solid black
border. We are passing through a link to the dark-mode Duke logo. You
will need to play around with bar_height
when you include
an image. By default, the function will pick-up the font family that
your theme uses for table headers. You can also mess around with
text_padding
, or you can switch image/text positions by
using text_align
or img_align
.
This is a really quick example. You can easily get something more descriptive with additional time or effort.
final_data %>%
group_by(stat_group) %>%
gt() %>%
cols_hide(team) %>%
gt_theme_savant() %>%
gt_color_pills(home,
domain = c(0, 1), format_type = "percent", digits = 1,
palette = "ggsci::green_material"
) %>%
gt_color_pills(away,
domain = c(0, 1), format_type = "percent", digits = 1,
palette = "ggsci::green_material"
) %>%
gt_538_caption("Data pulled using {cbbdata} from Barttorvik", "Viz. by @andreweatherman for a {gtUtils} vignette") %>%
cols_width(home:away ~ px(90)) %>%
cols_label(stat = "", home = "At Home", away = "On Road") %>%
cols_align(columns = -stat, "center") %>%
tab_style(locations = cells_body(columns = -stat), cell_text(weight = "bold")) %>%
tab_options(column_labels.border.top.style = "none") %>%
gt_border_bars_top(
colors = "black",
img = "https://a.espncdn.com/i/teamlogos/ncaa/500-dark/150.png",
text = "2024 Recap: Duke",
text_padding = 10,
text_size = 20,
img_width = 40,
img_height = 40,
bar_height = 60
)
Table Footers
Another possibility is using gt_border_bars_bottom
to
include a company or personal logo. For this example, let’s pull a
TidyTuesday dataset about Rolling Stone’s “500 Greatest Albums” and pull
the top 15.
data <- read_csv("https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2024/2024-05-07/rolling_stone.csv") %>%
slice_min(rank_2020, n = 15)
Again, this is a really quick and small example. The table itself is
pretty basic. We use gt_theme_sofa
on light mode, throw in
some row striping, and include both top and bottom bars. In the
bottom bar, we add some source text, push it 12px size, and toss it on
normal weight. The image we placed in the bottom could be
clearer, but there’s nothing that we can do about that. We stretch it to
100px wide and 30px tall. And with fewer than 20 lines of code, we have
a decent start to a table!
data %>%
mutate(album = paste0(rank_2020, ". ", album, " (", release_year, ")")) %>%
select(c(album, artist = clean_name, gender = artist_gender, genre)) %>%
gt(id = "table") %>%
gt_theme_sofa() %>%
sub_missing() %>%
cols_align(columns = "artist", "left") %>%
opt_all_caps() %>%
opt_row_striping() %>%
gt_border_bars_bottom("#1c2632",
bar_height = 25,
img = "https://www.sxsw.com/wp-content/uploads/2022/11/RollingStone-BW.png",
text = "Data pulled from Tidy Tuesday",
img_width = 100, img_height = 30, text_size = 12, text_weight = "normal"
) %>%
gt_border_bars_top("#1c2632", bar_height = 5) %>%
tab_header(
title = "The 15 Greatest Albums of All Time",
subtitle = md("Surveying the 2020 Rolling Stone's *500 Greatest Albums List*")
)
Just for fun
Another use of these functions is to add some pizzazz to your table.
With gt_border_bars
, you don’t need to add any text or
image! Let’s whip up a quick example and place some bars on the bottom
for fun.
If you do not specify text or an image, you can have as many bars as you’d like – all being placed with equal heights. They are plotted in sequential order, meaning that the first color in your vector is placed at the top and the last color is at the bottom.
data <- withr::with_seed(
seed = 10,
{
nycflights13::flights %>%
slice_sample(n = 15) %>%
select(1:8)
}
)
data %>%
gt() %>%
gt_theme_gtutils() %>%
tab_header("Example Title", "Example Subtitle") %>%
gt_border_bars_bottom(c("#EDBD68", "#F2704E", "#DE5152", "#A43845", "#602B53"))