3  Formatting

{gt} has two families of functions that handle a lot of the data formatting parts. And you have already seen members of these families, namely fmt_number() and sub_zero(). In this chapter, we’re going to discover some of their siblings.

The functions in these families are structured the same. So, if you can work with one, then you can work with them all. That’s why we’re not going to cover them all with examples here. For a full list of these functions take a look at the {gt} docs.

3.1 fmt_* functions

First, we need some example data to practice on. Thankfully, {gt} already comes with data sets that use many different data formats. Let me introduce you to {gt}’s example tibble, or exibble for short.

library(tidyverse)
library(gt)
exibble
## # A tibble: 8 × 9
##           num char       fctr  date       time  datetime    currency row   group
##         <dbl> <chr>      <fct> <chr>      <chr> <chr>          <dbl> <chr> <chr>
## 1       0.111 apricot    one   2015-01-15 13:35 2018-01-01…    50.0  row_1 grp_a
## 2       2.22  banana     two   2015-02-15 14:40 2018-02-02…    18.0  row_2 grp_a
## 3      33.3   coconut    three 2015-03-15 15:45 2018-03-03…     1.39 row_3 grp_a
## 4     444.    durian     four  2015-04-15 16:50 2018-04-04… 65100    row_4 grp_a
## 5    5550     <NA>       five  2015-05-15 17:55 2018-05-05…  1326.   row_5 grp_b
## 6      NA     fig        six   2015-06-15 <NA>  2018-06-06…    13.3  row_6 grp_b
## 7  777000     grapefruit seven <NA>       19:10 2018-07-07…    NA    row_7 grp_b
## 8 8880000     honeydew   eight 2015-08-15 20:20 <NA>            0.44 row_8 grp_b

Let’s put this into a {gt} table. We’re going to use one of the pre-defined themes that come with opt_stylize().

exibble |> 
  select(-(row:group)) |> 
  gt() |> 
  opt_stylize(style = 3)
num char fctr date time datetime currency
1.111e-01 apricot one 2015-01-15 13:35 2018-01-01 02:22 49.950
2.222e+00 banana two 2015-02-15 14:40 2018-02-02 14:33 17.950
3.333e+01 coconut three 2015-03-15 15:45 2018-03-03 03:44 1.390
4.444e+02 durian four 2015-04-15 16:50 2018-04-04 15:55 65100.000
5.550e+03 NA five 2015-05-15 17:55 2018-05-05 04:00 1325.810
NA fig six 2015-06-15 NA 2018-06-06 16:11 13.255
7.770e+05 grapefruit seven NA 19:10 2018-07-07 05:22 NA
8.880e+06 honeydew eight 2015-08-15 20:20 NA 0.440

Phew! This is table won’t win awards any time soon. Let’s clean it up by working us through the columns one by one.

3.1.1 Numbers

First, we’re getting rid of the scientific notation in the num column. While we’re at it, we’re going to round the numbers to one decimal.

exibble |> 
  select(num) |>
  gt() |> 
  opt_stylize(style = 3) |> 
  fmt_number(
    columns = 'num',
    decimals = 1
  )
num
0.1
2.2
33.3
444.4
5,550.0
NA
777,000.0
8,880,000.0

Next, we may want to adjust our marks , and . in the output. For example, in German we write one million as 1.000.000 and a quarter as 0,25. Hence, we could change the sep_mark and dec_mark argument in fmt_number(). But the easier way is to just change the locale to "de" (German).

exibble |> 
  select(num) |>
  gt() |> 
  opt_stylize(style = 3) |> 
  fmt_number(
    columns = 'num',
    decimals = 1,
    locale = 'de'
  )
num
0,1
2,2
33,3
444,4
5.550,0
NA
777.000,0
8.880.000,0

Since we also have some very large numbers in the num column, we could add suffixes instead of displaying a lot of zeroes. This means that we transform e.g. 1000 to 1K.

exibble |> 
  select(num) |> 
  gt() |> 
  opt_stylize(style = 3) |> 
  fmt_number(
    columns = 'num',
    decimals = 1,
    suffixing = TRUE
  )
num
0.1
2.2
33.3
444.4
5.5K
NA
777.0K
8.9M

We could also use our own suffixes.

# Thousand - Million - Billion - Trillion
custom_suffixes <- c("k", "mil", "bil", "tril")

exibble |> 
  select(num) |>
  gt() |> 
  opt_stylize(style = 3) |> 
  fmt_number(
    columns = 'num',
    decimals = 1,
    suffixing = custom_suffixes
  )
num
0.1
2.2
33.3
444.4
5.5k
NA
777.0k
8.9mil

3.1.2 Currency

Now, let’s format the currency column. The default currency is USD. That will give you $ signs.

exibble |> 
  select(num, currency) |> 
  gt() |> 
  opt_stylize(style = 3) |> 
  fmt_number(columns = 'num', decimals = 1) |>
  fmt_currency(columns = 'currency')
num currency
0.1 $49.95
2.2 $17.95
33.3 $1.39
444.4 $65,100.00
5,550.0 $1,325.81
NA $13.26
777,000.0 NA
8,880,000.0 $0.44

Since I mostly use Euros in my real life, let me change the currency argument here. Also, we’re going to set locale to German again.

exibble |> 
  select(num, currency) |> 
  gt() |> 
  opt_stylize(style = 3) |> 
  fmt_number(columns = 'num', decimals = 1) |>
  fmt_currency(
    columns = 'currency', currency = 'EUR', locale = 'de'
  )
num currency
0.1 €49,95
2.2 €17,95
33.3 €1,39
444.4 €65.100,00
5,550.0 €1.325,81
NA €13,26
777,000.0 NA
8,880,000.0 €0,44

You’d think that this is the correct way to state a price in Germany. But it’s not. Unfortunately, the locale did not catch that we use the Euro symbol at the end of a number. But no worries, we can fix that manually.

Instead of fmt_currency(), we’re going to use fmt_number() and apply the Euro symbol manually via pattern. The fmt_*() functions use {x} as placeholder for the function’s regular output. That way, we can modify outputs as we see fit. Here are two examples.

exibble |> 
  select(num, currency) |> 
  gt() |> 
  opt_stylize(style = 3) |> 
  fmt_number(columns = 'num', decimals = 1) |>
  fmt_number(
    columns = 'currency',
    decimals = 2,
    locale = 'de',
    pattern = '{x}€'
  )
num currency
0.1 49,95€
2.2 17,95€
33.3 1,39€
444.4 65.100,00€
5,550.0 1.325,81€
NA 13,26€
777,000.0 NA
8,880,000.0 0,44€
exibble |> 
  select(num, currency) |> 
  gt() |> 
  opt_stylize(style = 3) |> 
  fmt_number(columns = 'num', decimals = 1) |>
  fmt_number(
    columns = 'currency',
    decimals = 2,
    locale = 'de',
    pattern = 'EUR {x}'
  )
num currency
0.1 EUR 49,95
2.2 EUR 17,95
33.3 EUR 1,39
444.4 EUR 65.100,00
5,550.0 EUR 1.325,81
NA EUR 13,26
777,000.0 NA
8,880,000.0 EUR 0,44

We have rounded the num column to one decimal with the first fmt_number() layer. It’s interesting to find out what happens if we had targeted the currency column in that layer too. Would the next fmt_number() layer round the previously rounded number or the original number? Let’s check.

Code
exibble |> 
  select(num, currency) |> 
  gt() |> 
  opt_stylize(style = 3) |> 
  fmt_number(columns = c('num', 'currency'), decimals = 1) |>
  fmt_number(
    columns = 'currency',
    decimals = 2,
    locale = 'de',
    pattern = '{x}€'
  )
num currency
0.1 49,95€
2.2 17,95€
33.3 1,39€
444.4 65.100,00€
5,550.0 1.325,81€
NA 13,26€
777,000.0 NA
8,880,000.0 0,44€
Code
exibble |> 
  select(num, currency) |> 
  gt() |> 
  opt_stylize(style = 3) |> 
  fmt_number(columns = 'num', decimals = 1) |>
  fmt_number(
    columns = 'currency',
    decimals = 2,
    locale = 'de',
    pattern = '{x}€'
  )
num currency
0.1 49,95€
2.2 17,95€
33.3 1,39€
444.4 65.100,00€
5,550.0 1.325,81€
NA 13,26€
777,000.0 NA
8,880,000.0 0,44€

As you can see, the output is the same. This means that the fmt_*() functions always use the original data. That’s good to know.

Fun fact: That’s also what’s happening when you rename a column with cols_label(). Internally, the column names always remain the same.

3.1.3 Dates, times and datetimes

We can format any date using fmt_date(). And there are quite a few date_styles we can choose from. Here, are a few examples.

exibble |> 
  select(num, currency, date) |> 
  gt() |> 
  opt_stylize(style = 3) |> 
  fmt_number(columns = 'num', decimals = 1) |>
  fmt_number(
    columns = 'currency', 
    decimals = 2, 
    locale = 'de', 
    pattern = '{x}€'
  ) |> 
  fmt_date(columns = 'date', date_style = "wday_month_day_year")
num currency date
0.1 49,95€ Thursday, January 15, 2015
2.2 17,95€ Sunday, February 15, 2015
33.3 1,39€ Sunday, March 15, 2015
444.4 65.100,00€ Wednesday, April 15, 2015
5,550.0 1.325,81€ Friday, May 15, 2015
NA 13,26€ Monday, June 15, 2015
777,000.0 NA NA
8,880,000.0 0,44€ Saturday, August 15, 2015
exibble |> 
  select(num, currency, date) |> 
  gt() |> 
  opt_stylize(style = 3) |> 
  fmt_number(columns = 'num', decimals = 1) |>
  fmt_number(
    columns = 'currency', 
    decimals = 2, 
    locale = 'de', 
    pattern = '{x}€'
  ) |> 
  fmt_date(columns = 'date', date_style = "day_m_year")
num currency date
0.1 49,95€ 15 Jan 2015
2.2 17,95€ 15 Feb 2015
33.3 1,39€ 15 Mar 2015
444.4 65.100,00€ 15 Apr 2015
5,550.0 1.325,81€ 15 May 2015
NA 13,26€ 15 Jun 2015
777,000.0 NA NA
8,880,000.0 0,44€ 15 Aug 2015
exibble |> 
  select(num, currency, date) |> 
  gt() |> 
  opt_stylize(style = 3) |> 
  fmt_number(columns = 'num', decimals = 1) |>
  fmt_number(
    columns = 'currency', 
    decimals = 2, 
    locale = 'de', 
    pattern = '{x}€'
  ) |> 
  fmt_date(columns = 'date', date_style = "yMMMd")
num currency date
0.1 49,95€ Jan 15, 2015
2.2 17,95€ Feb 15, 2015
33.3 1,39€ Mar 15, 2015
444.4 65.100,00€ Apr 15, 2015
5,550.0 1.325,81€ May 15, 2015
NA 13,26€ Jun 15, 2015
777,000.0 NA NA
8,880,000.0 0,44€ Aug 15, 2015
exibble |> 
  select(num, currency, date) |> 
  gt() |> 
  opt_stylize(style = 3) |> 
  fmt_number(columns = 'num', decimals = 1) |>
  fmt_number(
    columns = 'currency', 
    decimals = 2, 
    locale = 'de', 
    pattern = '{x}€'
  ) |> 
  fmt_date(columns = 'date', date_style = "yMMMEd")
num currency date
0.1 49,95€ Thu, Jan 15, 2015
2.2 17,95€ Sun, Feb 15, 2015
33.3 1,39€ Sun, Mar 15, 2015
444.4 65.100,00€ Wed, Apr 15, 2015
5,550.0 1.325,81€ Fri, May 15, 2015
NA 13,26€ Mon, Jun 15, 2015
777,000.0 NA NA
8,880,000.0 0,44€ Sat, Aug 15, 2015

To see the full list of available styles, you can run info_date_style().

info_date_style()
Date Formatting Options
Usable in the fmt_date() and fmt_datetime() functions.

Format Name Formatted Date (en)
1 iso 2000-02-29
2 wday_month_day_year Tuesday, February 29, 2000
3 wd_m_day_year Tue, Feb 29, 2000
4 wday_day_month_year Tuesday 29 February 2000
5 month_day_year February 29, 2000
6 m_day_year Feb 29, 2000
7 day_m_year 29 Feb 2000
8 day_month_year 29 February 2000
9 day_month 29 February
10 day_m 29 Feb
11 year 2000
12 month February
13 day 29
14 year.mn.day 2000/02/29
15 y.mn.day 00/02/29
16 year_week 2000-W09
17 year_quarter 2000-Q1
18 yMd FLEXIBLE 2/29/2000
19 yMEd FLEXIBLE Tue, 2/29/2000
20 yMMM FLEXIBLE Feb 2000
21 yMMMM FLEXIBLE February 2000
22 yMMMd FLEXIBLE Feb 29, 2000
23 yMMMEd FLEXIBLE Tue, Feb 29, 2000
24 GyMd FLEXIBLE 2/29/2000 A
25 GyMMMd FLEXIBLE Feb 29, 2000 AD
26 GyMMMEd FLEXIBLE Tue, Feb 29, 2000 AD
27 yM FLEXIBLE 2/2000
28 Md FLEXIBLE 2/29
29 MEd FLEXIBLE Tue, 2/29
30 MMMd FLEXIBLE Feb 29
31 MMMEd FLEXIBLE Tue, Feb 29
32 MMMMd FLEXIBLE February 29
33 GyMMM FLEXIBLE Feb 2000 AD
34 yQQQ FLEXIBLE Q1 2000
35 yQQQQ FLEXIBLE 1st quarter 2000
36 Gy FLEXIBLE 2000 AD
37 y FLEXIBLE 2000
38 M FLEXIBLE 2
39 MMM FLEXIBLE Feb
40 d FLEXIBLE 29
41 Ed FLEXIBLE 29 Tue

Notice that some of these styles are labeled as flexible. This means that they will adjust to locales. Beware that month names may adapt to the locale but not the formatting.

Here’s an example of that with day_m_year (not flexible) and yMMMd (flexible) using the German locale. Notice how day_m_year does not set a . after the day but yMMMd does. The latter is the correct German formatting.

Code
exibble |> 
  select(num, currency, date) |> 
  gt() |> 
  opt_stylize(style = 3) |> 
  fmt_number(columns = 'num', decimals = 1) |>
  fmt_number(
    columns = 'currency', 
    decimals = 2, 
    locale = 'de',
    pattern = '{x}€'
  ) |> 
  fmt_date(
    columns = 'date', 
    locale = 'de',
    date_style = "day_m_year"
  )
num currency date
0.1 49,95€ 15 Jan. 2015
2.2 17,95€ 15 Feb. 2015
33.3 1,39€ 15 März 2015
444.4 65.100,00€ 15 Apr. 2015
5,550.0 1.325,81€ 15 Mai 2015
NA 13,26€ 15 Juni 2015
777,000.0 NA NA
8,880,000.0 0,44€ 15 Aug. 2015
Code
exibble |> 
  select(num, currency, date) |> 
  gt() |> 
  opt_stylize(style = 3) |> 
  fmt_number(columns = 'num', decimals = 1) |>
  fmt_number(
    columns = 'currency', 
    decimals = 2, 
    locale = 'de',
    pattern = '{x}€'
  ) |> 
  fmt_date(
    columns = 'date', 
    locale = 'de',
    date_style = "yMMMd"
  )
num currency date
0.1 49,95€ 15. Jan. 2015
2.2 17,95€ 15. Feb. 2015
33.3 1,39€ 15. März 2015
444.4 65.100,00€ 15. Apr. 2015
5,550.0 1.325,81€ 15. Mai 2015
NA 13,26€ 15. Juni 2015
777,000.0 NA NA
8,880,000.0 0,44€ 15. Aug. 2015

Formatting time works basically the same, so I’m just going to show one example.1

exibble |> 
  select(num, currency, date, time) |> 
  gt() |> 
  opt_stylize(style = 3) |> 
  fmt_number(columns = 'num', decimals = 1) |>
  fmt_number(
    columns = 'currency', 
    decimals = 2, 
    locale = 'de',
    pattern = '{x}€'
  ) |> 
  fmt_date(columns = 'date', locale = 'de', date_style = "yMMMd") |> 
  fmt_time(columns = 'time', time_style = "Hms")
num currency date time
0.1 49,95€ 15. Jan. 2015 13:35:00
2.2 17,95€ 15. Feb. 2015 14:40:00
33.3 1,39€ 15. März 2015 15:45:00
444.4 65.100,00€ 15. Apr. 2015 16:50:00
5,550.0 1.325,81€ 15. Mai 2015 17:55:00
NA 13,26€ 15. Juni 2015 NA
777,000.0 NA NA 19:10:00
8,880,000.0 0,44€ 15. Aug. 2015 20:20:00

I have a date. I have a time. Uh! Datetime, cf. PPAP2.

Working with these magical columns is exactly what you’d expect. You use fmt_datetime() which has a date_style and a time_style argument.

exibble |> 
  select(num, currency, date, time, datetime) |> 
  gt() |> 
  opt_stylize(style = 3) |> 
  fmt_number(columns = 'num', decimals = 1) |>
  fmt_number(
    columns = 'currency', 
    decimals = 2, 
    locale = 'de',
    pattern = '{x}€'
  ) |> 
  fmt_date(columns = 'date', locale = 'de', date_style = "yMMMd") |> 
  fmt_time(columns = 'time', time_style = "Hms") |> 
  fmt_datetime(
    columns = 'datetime', 
    date_style = "yMMMd", 
    time_style = "Hms"
  )
num currency date time datetime
0.1 49,95€ 15. Jan. 2015 13:35:00 Jan 1, 2018 02:22:00
2.2 17,95€ 15. Feb. 2015 14:40:00 Feb 2, 2018 14:33:00
33.3 1,39€ 15. März 2015 15:45:00 Mar 3, 2018 03:44:00
444.4 65.100,00€ 15. Apr. 2015 16:50:00 Apr 4, 2018 15:55:00
5,550.0 1.325,81€ 15. Mai 2015 17:55:00 May 5, 2018 04:00:00
NA 13,26€ 15. Juni 2015 NA Jun 6, 2018 16:11:00
777,000.0 NA NA 19:10:00 Jul 7, 2018 05:22:00
8,880,000.0 0,44€ 15. Aug. 2015 20:20:00 NA

3.1.4 Markdown

We can also use Markdown and therefore HTML + CSS in our tables. Let’s use that to make our table a bit colorful. For example, we could wrap elements from the currency column into <span>-tags to colorize them.

exibble |> 
  select(num, currency, date, time, datetime) |> 
  mutate(
    currency = str_c(
      '<span style="color:red;font-size:20pt">',
      currency,
      '€</span>'
    )
  ) |> 
  gt() |> 
  opt_stylize(style = 3) |> 
  fmt_number(columns = 'num', decimals = 1) |>
  fmt_date(columns = 'date', locale = 'de', date_style = "yMMMd") |> 
  fmt_time(columns = 'time', time_style = "Hms") |> 
  fmt_datetime(
    columns = 'datetime', 
    date_style = "yMMMd", 
    time_style = "Hms"
  ) |> 
  fmt_markdown(columns = 'currency')
num currency date time datetime
0.1 49.95€ 15. Jan. 2015 13:35:00 Jan 1, 2018 02:22:00
2.2 17.95€ 15. Feb. 2015 14:40:00 Feb 2, 2018 14:33:00
33.3 1.39€ 15. März 2015 15:45:00 Mar 3, 2018 03:44:00
444.4 65100€ 15. Apr. 2015 16:50:00 Apr 4, 2018 15:55:00
5,550.0 1325.81€ 15. Mai 2015 17:55:00 May 5, 2018 04:00:00
NA 13.255€ 15. Juni 2015 NA Jun 6, 2018 16:11:00
777,000.0 NA NA 19:10:00 Jul 7, 2018 05:22:00
8,880,000.0 0.44€ 15. Aug. 2015 20:20:00 NA

This is one way you could style your table. But I’ve used this way only for demo purposes. We’ll learn more about styling in Chapter 4.

The real power of the fmt_markdown() layer is that you can put any html into the table and it will be formatted correctly afterwards. For example, I’ve copied the svg-code (which can be used in HTML) for the R logo from Wikipedia. Putting this code a {gt} table and using fmt_markdown(), let’s me use the R logo.

## factor to apply to original width and height of svg from Wikipedia
scale_size <- 0.5 

r_logo_svg <- glue::glue('
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid" width="{724 * scale_size}" height="{561 * scale_size}" viewBox="0 0 724 561">
  <defs>
    <linearGradient id="gradientFill-1" x1="0" x2="1" y1="0" y2="1" gradientUnits="objectBoundingBox" spreadMethod="pad">
      <stop offset="0" stop-color="rgb(203,206,208)" stop-opacity="1"/>
      <stop offset="1" stop-color="rgb(132,131,139)" stop-opacity="1"/>
    </linearGradient>
    <linearGradient id="gradientFill-2" x1="0" x2="1" y1="0" y2="1" gradientUnits="objectBoundingBox" spreadMethod="pad">
      <stop offset="0" stop-color="rgb(39,109,195)" stop-opacity="1"/>
      <stop offset="1" stop-color="rgb(22,92,170)" stop-opacity="1"/>
    </linearGradient>
  </defs>
  <path d="M361.453,485.937 C162.329,485.937 0.906,377.828 0.906,244.469 C0.906,111.109 162.329,3.000 361.453,3.000 C560.578,3.000 722.000,111.109 722.000,244.469 C722.000,377.828 560.578,485.937 361.453,485.937 ZM416.641,97.406 C265.289,97.406 142.594,171.314 142.594,262.484 C142.594,353.654 265.289,427.562 416.641,427.562 C567.992,427.562 679.687,377.033 679.687,262.484 C679.687,147.971 567.992,97.406 416.641,97.406 Z" fill="url(#gradientFill-1)" fill-rule="evenodd"/>
  <path d="M550.000,377.000 C550.000,377.000 571.822,383.585 584.500,390.000 C588.899,392.226 596.510,396.668 602.000,402.500 C607.378,408.212 610.000,414.000 610.000,414.000 L696.000,559.000 L557.000,559.062 L492.000,437.000 C492.000,437.000 478.690,414.131 470.500,407.500 C463.668,401.969 460.755,400.000 454.000,400.000 C449.298,400.000 420.974,400.000 420.974,400.000 L421.000,558.974 L298.000,559.026 L298.000,152.938 L545.000,152.938 C545.000,152.938 657.500,154.967 657.500,262.000 C657.500,369.033 550.000,377.000 550.000,377.000 ZM496.500,241.024 L422.037,240.976 L422.000,310.026 L496.500,310.002 C496.500,310.002 531.000,309.895 531.000,274.877 C531.000,239.155 496.500,241.024 496.500,241.024 Z" fill="url(#gradientFill-2)" fill-rule="evenodd"/>
</svg>
')

tibble(logo = r_logo_svg) |> 
  gt() |> 
  fmt_markdown(columns = 'logo')

This fmt_markdown() technique is super powerful. We could even use it to nest {gt}-tables (which are HTML) inside of each other. That’s what we’ll do in Chapter 5 to create elaborate tables.

3.1.5 Any data format

There are some more fmt_*() functions for specific formats. Once again, you can look at them in the docs. Instead of showing them all, let me finish off this section with the most powerful function of them all. That’s fmt().

You can just apply any function that you like for formatting. For example, you could convert text entries to all-caps with str_to_upper().

exibble |> 
  select(num, currency, date, time, datetime, char) |> 
  gt() |> 
  opt_stylize(style = 3) |> 
  fmt_number(columns = 'num', decimals = 1) |>
  fmt_number(
    columns = 'currency', 
    decimals = 2, 
    locale = 'de',
    pattern = '{x}€'
  ) |> 
  fmt_date(columns = 'date', locale = 'de', date_style = "yMMMd") |> 
  fmt_time(columns = 'time', time_style = "Hms") |> 
  fmt_datetime(
    columns = 'datetime', 
    date_style = "yMMMd", 
    time_style = "Hms"
  ) |> 
  fmt(columns = 'char', fn = str_to_upper)
num currency date time datetime char
0.1 49,95€ 15. Jan. 2015 13:35:00 Jan 1, 2018 02:22:00 APRICOT
2.2 17,95€ 15. Feb. 2015 14:40:00 Feb 2, 2018 14:33:00 BANANA
33.3 1,39€ 15. März 2015 15:45:00 Mar 3, 2018 03:44:00 COCONUT
444.4 65.100,00€ 15. Apr. 2015 16:50:00 Apr 4, 2018 15:55:00 DURIAN
5,550.0 1.325,81€ 15. Mai 2015 17:55:00 May 5, 2018 04:00:00 NA
NA 13,26€ 15. Juni 2015 NA Jun 6, 2018 16:11:00 FIG
777,000.0 NA NA 19:10:00 Jul 7, 2018 05:22:00 GRAPEFRUIT
8,880,000.0 0,44€ 15. Aug. 2015 20:20:00 NA HONEYDEW

Or you could write your own time-formatting function.

on_time_format <- function(time, target) {
  if_else(parse_time(time) <= target, 'on time', 'too late')
}

exibble |> 
  select(time) |> 
  mutate(rep_time = time) |> 
  gt() |> 
  opt_stylize(style = 3) |> 
  fmt(
    columns = 'rep_time', 
    fns = function(x) {
      on_time_format(x, hms::hms(hours = 16, minutes = 30))
    }
  )
time rep_time
13:35 on time
14:40 on time
15:45 on time
16:50 too late
17:55 too late
NA NA
19:10 too late
20:20 too late

3.2 sub_ functions

The sub_*() functions are straightforward to use. There are five functions that you can use.

  • sub_missing() replaces NA values
  • sub_zero() replaces zeroes
  • sub_large_values() replaces large values (according to some threshold)
  • sub_small_values() does… I think you can guess it
  • sub_values() can replace large numbers or texts that match a regex

The first two are straight-forward to use. By default, they apply to the whole data. But you can also target only specific columns and rows by changing the columns and rows argument.

exibble |> 
  select(num, currency, date, time, datetime) |> 
  gt() |> 
  opt_stylize(style = 3) |> 
  fmt_number(columns = 'num', decimals = 1) |>
  fmt_number(
    columns = 'currency', 
    decimals = 2, 
    locale = 'de',
    pattern = '{x}€'
  ) |> 
  fmt_date(columns = 'date', locale = 'de', date_style = "yMMMd") |> 
  fmt_time(columns = 'time', time_style = "Hms") |> 
  fmt_datetime(
    columns = 'datetime', 
    date_style = "yMMMd", 
    time_style = "Hms"
  ) |> 
  sub_missing(missing_text = '----------')
num currency date time datetime
0.1 49,95€ 15. Jan. 2015 13:35:00 Jan 1, 2018 02:22:00
2.2 17,95€ 15. Feb. 2015 14:40:00 Feb 2, 2018 14:33:00
33.3 1,39€ 15. März 2015 15:45:00 Mar 3, 2018 03:44:00
444.4 65.100,00€ 15. Apr. 2015 16:50:00 Apr 4, 2018 15:55:00
5,550.0 1.325,81€ 15. Mai 2015 17:55:00 May 5, 2018 04:00:00
---------- 13,26€ 15. Juni 2015 ---------- Jun 6, 2018 16:11:00
777,000.0 ---------- ---------- 19:10:00 Jul 7, 2018 05:22:00
8,880,000.0 0,44€ 15. Aug. 2015 20:20:00 ----------
tibble(demo_column = -3:3) |> 
  gt() |> 
  opt_stylize(style = 3) |> 
  sub_zero(zero_text = 'ZERO, WATCH OUT WHOOP WHOOP')
demo_column
-3
-2
-1
ZERO, WATCH OUT WHOOP WHOOP
1
2
3

With sub_small_vals() and sub_large_vals() you have to be a bit careful about the sign of the number you’re replacing. Both functions will replace only positive or negative numbers. So, if you want to replace positive and negative numbers, you have to use the layers multiple times.

tibble(x = c(-100, 100, 0.01, -0.01), demo_col = x) |> 
  gt() |> 
  opt_stylize(style = 3) |> 
  fmt_number(columns = where(is.numeric)) |> 
  sub_small_vals(
    columns = 'demo_col', threshold = 1, sign = '+'
  ) |> 
  sub_large_vals(
    columns = 'demo_col', threshold = 50, sign = '+'
  )
x demo_col
−100.00 −100.00
100.00 ≥50
0.01 <1
−0.01 −0.01
tibble(x = c(-100, 100, 0.01, -0.01), demo_col = x) |> 
  gt() |> 
  opt_stylize(style = 3) |> 
  fmt_number(columns = where(is.numeric)) |> 
  sub_small_vals(
    columns = 'demo_col', threshold = 1, sign = '-'
  ) |> 
  sub_large_vals(
    columns = 'demo_col', threshold = 50, sign = '-'
  )
x demo_col
−100.00 ≤-50
100.00 100.00
0.01 0.01
−0.01 <abs(-1)
tibble(x = c(-100, 100, 0.01, -0.01), demo_col = x) |> 
  gt() |> 
  opt_stylize(style = 3) |> 
  fmt_number(columns = where(is.numeric)) |> 
  sub_small_vals(
    columns = 'demo_col', threshold = 1, sign = '+'
  ) |> 
  sub_large_vals(
    columns = 'demo_col', threshold = 50, sign = '+'
  ) |> 
  sub_small_vals(
    columns = 'demo_col', threshold = 1, sign = '-'
  ) |> 
  sub_large_vals(
    columns = 'demo_col', threshold = 50, sign = '-'
  )
x demo_col
−100.00 ≤-50
100.00 ≥50
0.01 <1
−0.01 <abs(-1)

The last sub_*() function is sub_values(). It is the most powerful function of the sub_*() family because it can replace numbers and texts. To do that it has a values and pattern argument. In case you’re wondering, you can only use one of them at a time. If you specify both, pattern will always take precedence.

But there’s more. It also has an fn argument. You could use it to let an arbitrary function decide which values get replaced. In order for this to work, this function must take a column and return a TRUE/FALSE vector of the same length.

Let’s take a look at a couple of examples.

exibble |>
  select(num) |> 
  mutate(demo_col = num) |> 
  gt() |> 
  opt_stylize(style = 3) |> 
  fmt_number(columns = everything()) |> 
  sub_values(
    columns = 'demo_col',
    values = c(0.111, 777000),
    replacement = 'REPLACED'
  )
num demo_col
0.11 0.11
2.22 2.22
33.33 33.33
444.40 444.40
5,550.00 5,550.00
NA NA
777,000.00 REPLACED
8,880,000.00 8,880,000.00
exibble |>
  select(char) |> 
  mutate(demo_col = char) |> 
  gt() |> 
  opt_stylize(style = 3) |> 
  sub_values(
    columns = 'demo_col',
    pattern = '(a|e)',
    replacement = 'fruit contains an a or e'
  )
char demo_col
apricot fruit contains an a or e
banana fruit contains an a or e
coconut coconut
durian fruit contains an a or e
NA NA
fig fig
grapefruit fruit contains an a or e
honeydew fruit contains an a or e
exibble |>
  select(num) |> 
  mutate(demo_col = num) |> 
  gt() |> 
  opt_stylize(style = 3) |> 
  fmt_number(columns = everything()) |> 
  sub_values(
    columns = 'demo_col',
    fn = function(x) between(x, 10, 10000),
    replacement = 'Between 10 and 10000'
  )
num demo_col
0.11 0.11
2.22 2.22
33.33 Between 10 and 10000
444.40 Between 10 and 10000
5,550.00 Between 10 and 10000
NA NA
777,000.00 777,000.00
8,880,000.00 8,880,000.00

3.3 Summary

That’s a wrap on Chapter 3. We’ve got the formatting options covered. Time to get to the most complicated part of our tables: Their theme.

Just like in a ggplot we can style more or less every part of our table. And if you’re familiar with HTML/CSS you can even apply custom styles that have not been implemented in {gt} yet.


  1. You should probably know that there seems to be an issue with some of the time formats when you’re also using {renv}. At least I’ve run into some troubles with that (see Issue). But if your desired format does not work, you can always format it manually. Either before sending the data to gt() or with fmt() which we’ll cover in a sec.↩︎

  2. My brain randomly reminded me of some dumb internet stuff from 6 years ago. So naturally I had to incorporate it into my text somehow. And of course the YouTube algorithm had to remind me of more fun stuff.↩︎