This blogpost will cover how to solve a fairly common ask, how to add a symbol/character to the end of ONLY the first row of a column and maintain the alignment of the entire column. We’ll walk through how to accomplish this with gt only, creating our own function to do it more succinctly, and then how to further test our gt outputs with testthat!
No repeats
I’ve always been a fan of not having to repeat symbols/prefixes/suffixes inside tables. There’s some ongoing work here in gt to add this as a feature, but in the meantime I wanted to play around with a few ways to accomplish this with gt as it is, and/or a custom function as of today.
You can imagine a situation like below, where we want to label cells within a column as a percent, and want to indicate that it’s a percent ONLY on the first row.
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
mfr
model
year
trim
hp
hp_pct
Ford
GT
2017
Base Coupe
647
97.88%
Ferrari
458 Speciale
2015
Base Coupe
597
90.32
Ferrari
458 Spider
2015
Base
562
85.02
Ferrari
458 Italia
2014
Base Coupe
562
85.02
Ferrari
488 GTB
2016
Base Coupe
661
100.00
Ferrari
California
2015
Base Convertible
553
83.66
However, you can quickly see that this misaligned the first row from the remaining rows.
No repeats in gt
An alternative would be to convert those rows to text and apply specific changes.
There’s quite a bit going on here:
Must use a mono space font for the column of interest
Must be mono-spaced so that everything aligns properly
Align the now text column to be right-aligned
Align to right, so again the decimal places align (text default aligns to left otherwise)
Must use " ", which is the HTML code for nonbreaking space, as a raw space (eg " ") will not work
I want to pause here and say with the code below, we have officially accomplished our goal. However, this was fairly manual and can be repetitive for adding several of these transformations in a single table.
head(gtcars)%>%mutate(hp_pct =(hp/max(hp)*100))%>%dplyr::select(mfr, model, year, trim, hp, hp_pct)%>%gt()%>%# use a mono-spaced fonttab_style(
style =cell_text(font =google_font("Fira Mono")),
locations =cells_body(columns =vars(hp_pct)))%>%# align the column of interst to rightcols_align(align ="right", columns =vars(hp_pct))%>%# round and transform the first row to percenttext_transform(
locations =cells_body(vars(hp_pct), rows =1),
fn =function(x){fmt_val<-format(as.double(x), nsmall =1, digits =1)paste0(fmt_val, "%")%>%gt::html()})%>%text_transform(
locations =cells_body(vars(hp_pct), rows =2:6),
fn =function(x){# round remaining rows, add a non-breaking spacefmt_val<-format(as.double(x), nsmall =1, digits =1)lapply(fmt_val, function(x)paste0(x, ' ')%>%gt::html())})
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
mfr
model
year
trim
hp
hp_pct
Ford
GT
2017
Base Coupe
647
97.9%
Ferrari
458 Speciale
2015
Base Coupe
597
90.3 
Ferrari
458 Spider
2015
Base
562
85.0 
Ferrari
458 Italia
2014
Base Coupe
562
85.0 
Ferrari
488 GTB
2016
Base Coupe
661
100.0 
Ferrari
California
2015
Base Convertible
553
83.7 
Format symbol first Function
We can try to wrap some of the gt code into a function and apply these transformations in bulk at the location of our choosing! This is especially important for making it generally apply to other types of inputs instead of JUST %. The function of interest is actually two custom functions, some gt functions, and a good chunk of logic.
I’ve commented the individual sections as to their purpose, and included quite a bit of error-handling or protecting against various user inputs.
fmt_symbol_first<-function(gt_data,
column=NULL, # column of interest to apply tosymbol=NULL, # symbol to add, optionallysuffix="", # suffix to add, optionallydecimals=NULL, # number of decimal places to round tolast_row_n, # what's the last row in data?symbol_first=FALSE# symbol before or after suffix?){# Test and error out if mandatory columns are missingstopifnot("`symbol_first` argument must be a logical"=is.logical(symbol_first))stopifnot("`last_row_n` argument must be specified and numeric"=is.numeric(last_row_n))stopifnot("Input must be a gt table"=class(gt_data)[[1]]=="gt_tbl")# needs to type convert to double to play nicely with decimals and rounding# as it's converted to character by gt::text_transformadd_to_first<-function(x, suff=suffix, symb=symbol){if(!is.null(decimals)){x<-suppressWarnings(as.double(x))fmt_val<-format(x =x, nsmall =decimals, digits =decimals)}else{fmt_val<-x}# combine the value, passed suffix, symbol -> htmlif(isTRUE(symbol_first)){paste0(fmt_val, symb, suff)%>%gt::html()}else{paste0(fmt_val, suff, symb)%>%gt::html()}}# repeat non-breaking space for combined length of suffix + symbol# logic is based on is a NULL passed or notif(!is.null(symbol)|!identical(as.character(symbol), character(0))){suffix<-ifelse(identical(as.character(suffix), character(0)), "", suffix)length_nbsp<-c(" ", rep(" ", nchar(suffix)))%>%paste0(collapse ="")}else{suffix<-ifelse(identical(as.character(suffix), character(0)), "", suffix)length_nbsp<-rep(" ", nchar(suffix))%>%paste0(collapse ="")}# affect rows OTHER than the first rowadd_to_remainder<-function(x, length=length_nbsp){if(!is.null(decimals)){# if decimal not null, convert to doublex<-suppressWarnings(as.double(x))# then round and format ALL to force specific decimalsfmt_val<-format(x =x, nsmall =decimals, digits =decimals)}else{fmt_val<-x}paste0(fmt_val, length)%>%lapply(FUN =gt::html)}# pass gt object# align right to make sure the spacing is meaningfulgt_data%>%cols_align(align ="right", columns =vars({{column}}))%>%# convert to mono-font for column of interesttab_style(
style =cell_text(font =google_font("Fira Mono")),
locations =cells_body(columns =vars({{column}})))%>%# transform first rowstext_transform(
locations =cells_body(vars({{column}}), rows =1),
fn =add_to_first)%>%# transform remaining rowstext_transform(
locations =cells_body(vars({{column}}), rows =2:last_row_n),
fn =add_to_remainder)}
Use the function
We can now use that fmt_symbol_first() function, note that I’m testing a few different combinations of suffix/symbols, decimals, etc that may be a bit nonsensical in the table itself but are interactively testing that the results are what I expect. Specifically, I’m making sure that symbols/suffixes are added, and that the spacing is correct. While this is useful for sanity checking quickly, we can also take another step to apply some proper unit-testing in the next section.
gtcars%>%head()%>%dplyr::select(mfr, year, bdy_style, mpg_h, hp)%>%dplyr::mutate(mpg_h =rnorm(n =dplyr::n(), mean =22, sd =1))%>%gt()%>%opt_table_lines()%>%fmt_symbol_first(column =mfr, symbol ="$", suffix =" ", last_row_n =6)%>%fmt_symbol_first(column =year, symbol =NULL, suffix ="%", last_row_n =6)%>%fmt_symbol_first(column =mpg_h, symbol ="%", suffix =NULL, last_row_n =6, decimals =1)%>%fmt_symbol_first(column =hp, symbol ="°", suffix ="F", last_row_n =6, decimals =NULL, symbol_first =TRUE)
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
mfr
year
bdy_style
mpg_h
hp
Ford $
2017%
coupe
23.4%
647°F
Ferrari  
2015 
coupe
22.1 
597  
Ferrari  
2015 
convertible
21.4 
562  
Ferrari  
2014 
coupe
23.7 
562  
Ferrari  
2016 
coupe
23.1 
661  
Ferrari  
2015 
convertible
22.3 
553  
Unit testing
At this point, we’ve created a custom gt wrapper function, added some relatively robust checks into the function, but are still manually checking the output confirms to our expectations. We can perform proper unit testing with the {testthat} package.
Testing your code can be painful and tedious, but it greatly increases the quality of your code. testthat tries to make testing as fun as possible, so that you get a visceral satisfaction from writing tests.
While an in-depth run through of testhat is beyond the scope of this post, I have included an expandable section with a minimal example below, expanded from the “R Packages” book chapter on testing:
So str_length() counts the length of a string, fairly straightforward!
We can convert this to a logical confirmation, which means that a computer can understand if the output was as expected, rather than just printing and reading which is mainly for our interactive use. I have included one FALSE output just as an example.
While this testing is useful, we can make it even easier with testhat, by using expect_equal(). Now, these functions will not return anything if they pass. If they fail, then they will print an error, and a helpful statement saying what the failure was.
Error: str_length("a") not equal to 3.
1/1 mismatches
[1] 1 - 3 == -2
The last step, is wrapping our various tests into test_that structure. Here, while the individual tests return no visible output, we can get a friendly message saying they have all passed!
── Failure (<text>:7:5): str_length is number of characters ────────────────────
str_length("abcd") not equal to 3.
1/1 mismatches
[1] 4 - 3 == 1
Error:
! Test failed
These tests can be used interactively, but ultimately are even more useful when rolled into an R package. For that next step, I recommend reading through the “R Packages” book, specifically the Packages Chapter.
Testing gt
Now you may say, well those minimal example tests were easy, it’s just counting?! How do I test gt? We can treat gt exactly like what it is, a HTML table. Quick example below using our custom function (fmt_symbol_first()).
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead
Warning: `columns = vars(...)` has been deprecated in gt 0.3.0:
* please use `columns = c(...)` instead