class: title-slide, left, top # Beautiful tables ## with `gt` and `gtExtras` ### Tom Mock <br>
[jthomasmock.github.io/gtExtras-prez/](https://jthomasmock.github.io/gtExtras-prez/)
[github.com/jthomasmock/gtExtras-prez](https://github.com/jthomasmock/gtExtras-prez) <span style='color:white;'>Slides released under</span> [CC-BY 2.0](https://creativecommons.org/licenses/by/2.0/)
] <div style = "position: absolute;top: 0px;right: 0px;"><img src="https://raw.githubusercontent.com/jthomasmock/gtExtras-prez/main/images/table-stack.png" alt="The hex logo for plumbertableau package" width="600"></img></div> --- layout: true <div class="my-footer"><span>https://github.com/jthomasmock/gtExtras-prez/</span></div> --- ### RStudio Table contests * [2021 Announcement](https://blog.rstudio.com/2021/09/30/rstudio-table-contest-2021/) * [2020 Results](https://blog.rstudio.com/2020/12/23/winners-of-the-2020-rstudio-table-contest/) --- class: center # Why do we care about tables? --- class: center ### *Why do we care about tables?* # Why do we care about graphs? --- class: center ### *Why do we care about tables?* ### *Why do we care about graphs?* # *Both* Graphs AND Tables *are* tools for communication --- class: center ### Why do we care about tables? ### Why do we care about graphs? ### Both Graphs and tables are tools for communication # Better Graphs/Tables *are* better communication --- ### A **grammar** of graphics * `ggplot2` is an application of the **grammar of graphics** for R -- * A default dataset and set of mappings from variables to aesthetics * One or more layers of geometric objects * One scale for each aesthetic mapping used * A coordinate system * The facet specification --- ### A **grammar** of graphics, from [**ggplot2 as a creativity engine**](https://johnburnmurdoch.github.io/slides/r-ggplot/#/) .pull-left[ Easy enough to [*rapidly prototype*](https://johnburnmurdoch.github.io/slides/r-ggplot/#/14) graphics at the "speed of thought" <img src="https://johnburnmurdoch.github.io/slides/r-ggplot/football-tide-2.png" title="A screenshot of a scatterplot with team rank on the x-axis and team rating on the y-axis. The teams are all trending down from rank 1 to rank 20." alt="A screenshot of a scatterplot with team rank on the x-axis and team rating on the y-axis. The teams are all trending down from rank 1 to rank 20." width="85%" /> ] -- .pull-right[ Powerful enough for [*final "publication"*](https://johnburnmurdoch.github.io/slides/r-ggplot/#/34) <img src="http://blogs.ft.com/ftdata/files/2016/03/eng.png" title="A facetted scattergraph by year for the premier league. Team rating is on the Y-axis and rank is on the x-axis. The teams overall are declining." alt="A facetted scattergraph by year for the premier league. Team rating is on the Y-axis and rank is on the x-axis. The teams overall are declining." width="70%" /> ] --- ### A **grammar** of tables -- Construct a wide variety of useful tables with a cohesive set of table parts. These include the *table header*, the *stub*, the *column labels* and *spanner column labels*, the *table body* and the *table footer*. -- ![A diagram of the components of a table. Notably, there are specific sections for every single component of a table, providing a rich toolbox for composing and modifying tables in R](https://gt.rstudio.com/reference/figures/gt_parts_of_a_table.svg) --- ![A diagram of the typical gt workflow, which is passing tabular data into gt() function, creating a gt object and can then be displayed as HTML](https://gt.rstudio.com/reference/figures/gt_workflow_diagram.svg) -- .pull-left[ Easy enough to *rapidly prototype* <img src="https://themockup.blog/static/slides/images/urban/prototype.png" title="a basic gt table of mtcars" alt="a basic gt table of mtcars" width="40%" /> ] -- .pull-right[ Powerful enough for *final "publication"* <img src="https://themockup.blog/static/slides/images/qbr_win_tab.png" width="30%" /> ] --- class:inverse, center, middle # Best practices --- ### 10 "Rules", adapted from Jon Schwabish .pull-left[ #### 1. Offset the Heads from the Body #### 2. Use Subtle Dividers over Heavy Grids #### 3. Right-Align Numbers #### 4. Left-Align Text #### 5. Select Appropriate Precision ] .pull-right[ #### 6. Guide your Reader with Space between Rows and Columns #### 7. Remove Unit Repetition #### 8. Highlight Outliers #### 9. Group Similar Data and Increase White Space #### 10. Add Visualizations when Appropriate ] --- ### Tables vs Graphs ![When to use a table vs a graph according to Stephen Few. Notably, tables are used to display exact values, while graphs are best for showing trends or patterns.](https://themockup.blog/static/slides/tables/few-table-rule.png) --- class: center, middle, inverse # `gtExtras` is all about bending the rules --- ### Graph or table? <img src="https://pbs.twimg.com/media/FATujGvVkAEDlOa?format=png&name=medium" title="A complex table with inlaid graphics" alt="A complex table with inlaid graphics" width="55%" /> --- ### Graph or table? <img src="https://pbs.twimg.com/media/FAjGvRIXIAAv-ll?format=png" title="A complex table with inline graphics" alt="A complex table with inline graphics" width="50%" /> --- ### Graphs _and_ tables There are 7 plotting functions in `gtExtras`: * `gt_plt_winloss()` Add win loss point plot into rows of a gt table * `gt_sparkline()` Add sparklines into rows of a gt table * `gt_plt_bar()` Add bar plots into rows of a gt table * `gt_plt_bar_pct()` Add HTML-based bar plots into rows of a gt table * `gt_plt_bar_stack()` Add a percent stacked barchart in place of existing data * `gt_plt_bullet()` Create an inline 'bullet chart' in a gt table * `gt_plt_dot()` Add a color dot and thin bar chart to a table --- ### `gt_plt_winloss()` .pull-left[ ```r set.seed(37) data_in <- dplyr::tibble( grp = rep(c("A", "B", "C"), each = 10), wins = sample( c(0,1,.5), size = 30, prob = c(0.45, 0.45, 0.1), replace = TRUE) ) %>% dplyr::group_by(grp) %>% dplyr::summarize(wins=list(wins), .groups = "drop") data_in ``` ``` ## # A tibble: 3 × 2 ## grp wins ## <chr> <list> ## 1 A <dbl [10]> ## 2 B <dbl [10]> ## 3 C <dbl [10]> ``` ] -- .pull-right[ ```r win_table <- data_in %>% gt() %>% gt_plt_winloss(wins) win_table ```
grp
wins
A
B
C
] --- .left-narrow[ ```r final_df %>% glimpse() ``` ``` ## Rows: 16 ## Columns: 5 ## $ team_wordmark <chr> "https://github.com/nflverse/nflfastR-data/raw/master/wo… ## $ team_division <fct> North, North, North, North, South, South, South, South, … ## $ Wins <int> 12, 11, 11, 4, 11, 11, 4, 1, 13, 10, 7, 2, 14, 8, 7, 5 ## $ Losses <int> 4, 5, 5, 11, 5, 5, 12, 15, 3, 6, 9, 14, 2, 8, 9, 11 ## $ outcomes <list> <1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0>, <1, 1,… ``` ] --- .pull-left[ ```r gt(final_df, groupname_col = "team_division") %>% gtExtras::gt_plt_winloss( outcomes, max_wins = 16, type = "pill") %>% gtExtras::gt_img_rows(team_wordmark, height = 20) %>% gtExtras::gt_theme_538() %>% cols_label(team_wordmark = "") %>% cols_align("left", team_division) %>% tab_header( title = gtExtras::add_text_img( "2020 Results by Division", url = "https://github.com/nflverse/nflfastR-data/raw/master/AFC.png", height = 30 ) ) %>% tab_options(data_row.padding = px(2)) %>% gtsave("images/gt-plt-winloss.png") ``` ] -- .pull-right[ <img src="images/gt-plt-winloss.png" title="A table with inline indicators for wins and losses based on small pillshapes" alt="A table with inline indicators for wins and losses based on small pillshapes" width="70%" /> ] --- ### `gt_sparkline()` ```r gt_sparkline_tab <- mtcars %>% dplyr::group_by(cyl) %>% # must end up with list of data for each row in the input dataframe dplyr::summarize( across(c(mpg, disp, hp), mean, .names = "{.col}_avg"), mpg_data = list(mpg), .groups = "drop") %>% gt() %>% gt_sparkline(mpg_data, same_limit = FALSE) gt_sparkline_tab %>% fmt_number(contains("avg"), decimals = 1) ```
cyl
mpg_avg
disp_avg
hp_avg
mpg_data
4
26.7
105.1
82.6
6
19.7
183.3
122.3
8
15.1
353.1
209.2
--- ### `gt_plt_bar()` ```r head(mtcars) %>% select(cyl, disp, hp, mpg) %>% gt() %>% gt_plt_bar(column = mpg, keep_column = TRUE, color = "red") ```
cyl
disp
hp
mpg
mpg
6
160
110
21.0
6
160
110
21.0
4
108
93
22.8
6
258
110
21.4
8
360
175
18.7
6
225
105
18.1
--- ### `gt_plot_bar_stack()` ```r ex_df <- dplyr::tibble( x = c("Example 1","Example 1", "Example 1","Example 2","Example 2","Example 2", "Example 3","Example 3","Example 3","Example 4","Example 4","Example 4"), measure = c("Measure 1","Measure 2", "Measure 3","Measure 1","Measure 2", "Measure 3", "Measure 1","Measure 2","Measure 3","Measure 1", "Measure 2","Measure 3"), data = c(30, 20, 50, 30, 30, 40, 30, 40, 30, 30, 50, 20) ) tab_df <- ex_df %>% group_by(x) %>% summarise(list_data = list(data), .groups = "drop") tab_df ``` ``` ## # A tibble: 4 × 2 ## x list_data ## <chr> <list> ## 1 Example 1 <dbl [3]> ## 2 Example 2 <dbl [3]> ## 3 Example 3 <dbl [3]> ## 4 Example 4 <dbl [3]> ``` --- ### `gt_plt_bar_stack()` ```r tab_df %>% gt() %>% gt_plt_bar_stack(column = list_data) ```
x
Group 1
||
Group 2
||
Group 3
Example 1
30
20
50
Example 2
30
30
40
Example 3
30
40
30
Example 4
30
50
20
--- ### `gt_plot_bar_stack()` ```r ncaa_df <- player_df %>% group_by(player) %>% summarise( across(-contains("shot"), unique), data = list(shot_mix), .groups = "drop" ) %>% mutate(player = factor(player, levels = rev(player_lvls))) %>% arrange(player) ncaa_df ``` ``` ## # A tibble: 8 × 10 ## player team ht dk_pct_time dk_pps tip_pct_time tip_pps jmp_pct_time ## <fct> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> ## 1 Evan Mobl… USC "7'0… 40 1.62 26 0.88 33 ## 2 Sandro Ma… Seton H… "6'1… 48 1.02 10 0.97 42 ## 3 Charles B… Western… "6'1… 50 1.54 19 1 31 ## 4 Luke Garza Iowa "6'1… 50 1.33 15 1.05 35 ## 5 Moses Wri… Georgia… "6'9… 51 1.46 25 0.63 25 ## 6 Neemias Q… Utah St "7'1… 55 1.37 27 0.85 18 ## 7 Isaiah Ja… Kentucky "6'1… 60 1.33 15 0.76 25 ## 8 Day'Ron S… North C… "6'1… 66 1.18 24 0.84 10 ## # … with 2 more variables: jmp_pps <dbl>, data <list> ``` --- ### NCAA Table, code [here](https://gist.github.com/jthomasmock/6976d589572eaf6db3d13cfc9127a8b7) <img src="images/ncaa-tab.png" title="A complex table that has stacked bar chart in one row, highlighting the ability to create complex tables with inline bars" alt="A complex table that has stacked bar chart in one row, highlighting the ability to create complex tables with inline bars" width="85%" /> --- ### `gt_plt_dot()` ```r head(mtcars) %>% tibble::rownames_to_column("cars") %>% dplyr::select(cars, mpg, disp) %>% dplyr::mutate(cars = word(cars, 1)) %>% gt() %>% gt_plt_dot(disp, cars, palette = "ggthemes::fivethirtyeight") %>% cols_width(cars ~ px(125)) ```
cars
mpg
disp
Mazda
21.0
160
Mazda
21.0
160
Datsun
22.8
108
Hornet
21.4
258
Hornet
18.7
360
Valiant
18.1
225
--- ### `gt_plt_dot()` with [Code](https://gist.github.com/jthomasmock/b6fd5d64842296de2d1ef9cad6769f2c) <img src="https://user-images.githubusercontent.com/29187501/134953158-0b71ca46-bda1-4d3f-a1ce-e879fd972a13.png" title="A table with a plot-dot, ie a thin bar chart below the values in the row" alt="A table with a plot-dot, ie a thin bar chart below the values in the row" width="50%" /> ```r library(gt) library(gtExtras) library(dplyr) library(htmltools) # original source: https://www.bloomberg.com/graphics/2021-german-election-results/ party_df <- tibble( Party = c("SPD", "CDU/CSU", "Greens", "FDP", "AfD", "Left", "Other"), Seats = c(206, 196, 118, 92, 83, 39, 1), `% of 2nd Votes` = c(25.7, 24.1, 14.8, 11.5, 10.3, 4.9, 8.7) ) minimal_table <- gt(party_df) %>% gt_plt_dot(column = Seats, category_column = Party, max_value = 379, palette = c("#ec323f", "black", "#63d64a", "#fff24e", "#4fabf7", "#e956ad", "grey")) %>% gtExtras::gt_theme_nytimes() %>% tab_header(title = "Results by Party in the Bundestag Election", subtitle = "Seats and votes are based on provisional official results.") %>% cols_width(Party ~ px(368), 3 ~ px(30)) party_table <- gt(party_df) %>% gt_plt_dot(column = Seats, category_column = Party, max_value = 368, palette = c("#ec323f", "black", "#63d64a", "#fff24e", "#4fabf7", "#e956ad", "grey")) %>% gtExtras::gt_theme_nytimes() %>% tab_header(title = "Results by Party in the Bundestag Election", subtitle = "Seats and votes are based on provisional official results.") %>% cols_width(Party ~ px(300), 3 ~ px(30)) %>% tab_style(style = list(cell_text(color = "grey"),cell_borders(color = "white")), locations = cells_body(3)) %>% tab_source_note( html( paste0( "With a total of 735 seats<br>", "<span style='color:#bfbfbf;'>Data as of: Sept 26, 2021, 11:09PM CDT</span>" ) ) ) %>% tab_style(style = cell_borders("right", "lightgrey", "dashed"), cells_body(Party)) %>% tab_style(style = cell_borders("top", "white"), cells_body(rows = 1)) %>% tab_options(table.border.bottom.color = "white") combo_table <- htmltools::div( party_table, htmltools::div( "368 seats for majority", style = paste0( htmltools::css( background= "white", font.size = px(11), width = px(60), font.family = "arial", display = "flex", text.align = "center", color = "#999", position = "fixed", top = "230px", left = "290px" ) ) ) ) ``` --- class:center,middle,inverse # Not all about graphs! --- ### Themes Just like `ggplot2`, we can craft themes but for tables! Themes from: * ESPN * FiveThirtyEight * NY Times * The Guardian * A dot matrix printer * Dark theme * Excel style --- ### `gt_theme_538()` ![A screenshot of a table with formatting similar to tables from FiveThirtyEight](https://jthomasmock.github.io/gtExtras/reference/figures/gt_538.png) --- ### `gt_theme_espn()` ![A screenshot of a table with formatting similar to ESPN](https://jthomasmock.github.io/gtExtras/reference/figures/gt_espn.png) --- ### `gt_theme_nytimes()` <img src="https://jthomasmock.github.io/gtExtras/reference/figures/gt_nyt.png" title="A screenshot of a table with formatting similar to NY Times" alt="A screenshot of a table with formatting similar to NY Times" width="65%" /> --- ### `gt_theme_guardian()` ![A screenshot of a table with formatting similar to the Guardian](https://jthomasmock.github.io/gtExtras/reference/figures/gt_guardian.png) --- ### `gt_theme_dot_matrix()` ![A screenshot of a table with formatting similar to a dot matrix printer](https://jthomasmock.github.io/gtExtras/reference/figures/gt_dot_matrix.png) --- ### `gt_theme_dark()` <img src="https://jthomasmock.github.io/gtExtras/reference/figures/gt_dark.png" title="A screenshot of a table with a dark theme, ie all dark grey with white text" alt="A screenshot of a table with a dark theme, ie all dark grey with white text" width="65%" /> --- ### `gt_theme_excel()` <img src="https://jthomasmock.github.io/gtExtras/reference/figures/gt_excel.png" title="A screenshot of a table with formatting similar to Excel" alt="A screenshot of a table with formatting similar to Excel" width="80%" /> --- class: center, middle, inverse # Utilities, to do one thing easier --- ### `fmt_symbol_first()` .pull-left[ ```r head(gtcars) %>% select(mfr, year, bdy_style, mpg_h, hp) %>% mutate(mpg_h = rnorm( n = dplyr::n(), mean = 22, sd = 1 )) %>% gt() %>% opt_table_lines() %>% fmt_symbol_first(column = mfr, symbol = "$") %>% fmt_symbol_first(year, suffix = "%") %>% fmt_symbol_first(mpg_h, symbol = "%", decimals = 1) %>% fmt_symbol_first(hp, symbol = "°", suffix = "F", symbol_first = TRUE) ``` ] -- .pull-right[ ![A table highlighting that there are only symbols on the first row, while the other rows are properly aligned still](https://jthomasmock.github.io/gtExtras/reference/figures/gt_fmt_first.png) ] --- ### `gt_highlight_rows()` .pull-left[ ```r sub_cars <- head(mtcars[,1:5]) %>% tibble::rownames_to_column("car") gt(sub_cars) %>% gt_highlight_rows(rows = 2, font_weight = "normal") ``` <img src="https://jthomasmock.github.io/gtExtras/reference/figures/highlight-basic.png" title="A screenshot of table with a specific row highlighted, the row has light blue background" alt="A screenshot of table with a specific row highlighted, the row has light blue background" width="80%" /> ] -- .pull-right[ ```r gt(sub_cars) %>% gt_highlight_rows( rows = car == "Valiant", fill = "lightgrey", target_col = car, bold_target_only = TRUE ) ``` <img src="images/gt-bold-target.png" title="A screenshot of a table with a specific row highlighted, the row has a lightgrey background and only the target column has bold text." alt="A screenshot of a table with a specific row highlighted, the row has a lightgrey background and only the target column has bold text." width="90%" /> ] --- ### `gt_merge_stack()` & `gt_img_rows()` .pull-left[ ```r teams <- "https://github.com/nflverse/nflfastR-data/raw/master/teams_colors_logos.rds" team_df <- readRDS(url(teams)) head(team_df, 8) %>% select(team_nick, team_abbr, team_conf, team_division, team_wordmark) %>% gt(groupname_col = "team_conf") %>% gt_merge_stack(col1 = team_nick, col2 = team_division) %>% gt_img_rows(team_wordmark) ``` ] -- .pull-right[ <img src="https://jthomasmock.github.io/gtExtras/reference/figures/merge-stack.png" title="A screen shot of a complex table, where two columns have been merged into a single column. The text from col1 is on top and the text from col2 is below it" alt="A screen shot of a complex table, where two columns have been merged into a single column. The text from col1 is on top and the text from col2 is below it" width="60%" /> ] --- class: center, middle, inverse # What's in a `gtExtras` function anyway? --- ### `gtExtras` is just several `gt` functions in a trenchcoat * `gtExtras` is _just_ pure `gt` with boilerplate repeated * `gtExtras` uses `ggplot2` -> SVG -> HTML for plots * `gtExtras` uses custom HTML/CSS to "do extra things" -- * `gtExtras` uses several `gt` functions to do one cool thing at a time! -- * `gt` accepts `tidyeval` -- * `tidyeval` is a scary word for `{{ var }}` --- ### `tidyeval` in 30 seconds ```r my_dplyr_summary <- function(data, var, ...){ data %>% * group_by({{ var }}) %>% summarize(n = n(), ...) } ``` -- .pull-left[ ```r mtcars %>% my_dplyr_summary(cyl, avg_fuel_eff = mean(mpg)) ``` ``` ## # A tibble: 3 × 3 ## cyl n avg_fuel_eff ## <dbl> <int> <dbl> ## 1 4 11 26.7 ## 2 6 7 19.7 ## 3 8 14 15.1 ``` ] -- .pull-right[ ```r ToothGrowth %>% my_dplyr_summary( dose, avg_len = mean(len), sd_len = sd(len)) ``` ``` ## # A tibble: 3 × 4 ## dose n avg_len sd_len ## <dbl> <int> <dbl> <dbl> ## 1 0.5 20 10.6 4.50 ## 2 1 20 19.7 4.42 ## 3 2 20 26.1 3.77 ``` ] --- ### Just _embrace_ all the `{{ things }}` The embrace or "curly curly" indicates to `dplyr` and friends that you are indicating some object inside the data. In this case, a column. -- If you need to do more than one column, you can use `...`, or "passing the dots". This allows _anything_ to be passed forward and delays evaluation. --- ### `tidyeval` applied to `gt` ```r gt_theme_dot_matrix <- function(gt_object, ..., color = "#b5dbb6"){ gt_object %>% opt_row_striping() %>% tab_style( style = cell_borders(sides = "bottom", weight = px(3), color = "white"), locations = list(cells_body(rows = nrow(gt_object[["_data"]])))) %>% opt_table_font(font = "Courier") %>% tab_options( * ..., # PASSING THE DOTS heading.align = "left", heading.border.bottom.color = "white", column_labels.text_transform = "lowercase", column_labels.font.size = pct(85), column_labels.border.top.style = "none", column_labels.border.bottom.color = "black", column_labels.border.bottom.width = px(2), table.border.bottom.style = "none", table.border.bottom.width = px(2), table.border.bottom.color = "white", table.border.top.style = "none", row.striping.background_color = color, table_body.hlines.style = "none", table_body.vlines.style = "none", data_row.padding = px(1) ) } ``` --- ### `tidyeval` applied to `gt` ```r gt_highlight_rows <- function(gt_object, columns = gt::everything(), rows = TRUE, fill = "#80bcd8", alpha = 0.8, font_weight = "bold"){ stopifnot("Table must be of class 'gt_tbl'" = "gt_tbl" %in% class(gt_object)) gt_object %>% tab_style( style = cell_fill(color = fill, alpha = alpha), # TIDYEVAL * locations = cells_body(columns = {{ columns }},rows = {{ rows }}) ) %>% tab_style( style = list(cell_text(weight = font_weight)), # TIDYEVAL * locations = cells_body(columns = {{ target_col }},rows = {{ rows }}) ) } ``` --- ### `gt` is amazing * `gtExtras` is just wrapping `gt` to make very specific tasks more succinct * You can write your own `gt` functions! * To read more about `tidyeval`, see my [slides](https://jthomasmock.github.io/pkg-building/#41) -- ### Followup * [`gtExtras` docs](https://jthomasmock.github.io/gtExtras/) * [`gt` docs](https://gt.rstudio.com/) * [My `gt` blogposts](https://themockup.blog/#category:gt)