17 Flextable

17.1 ⭐️Overview

Using the flextable package

Some overlap with:

17.3 📦Load packages

library(dplyr, warn.conflicts = FALSE)
library(officer, warn.conflicts = FALSE)
library(flextable, warn.conflicts = FALSE)

17.4 Load data

data("mtcars")

17.5 🔵 i and j

i = rows
j = columns

17.6 🔵 Flextable wrappers

We can wrap flextable functions that we use repeatedly in a wrapper.

my_ft_theme <- function(ft, ...) {
  # Remove vertical cell padding
  ft <- padding(ft, padding.top = 0, padding.bottom = 0, part = "all")
  
  # Change font to TNR 11
  ft <- font(ft, fontname = "Times New Roman", part = "all")
  ft <- fontsize(ft, part = "all", size = 11)
  ft
}

And then use it like this:

flextable(head(mtcars)) %>%
  my_ft_theme()

mpg

cyl

disp

hp

drat

wt

qsec

vs

am

gear

carb

21.0

6

160

110

3.90

2.620

16.46

0

1

4

4

21.0

6

160

110

3.90

2.875

17.02

0

1

4

4

22.8

4

108

93

3.85

2.320

18.61

1

1

4

1

21.4

6

258

110

3.08

3.215

19.44

1

0

3

1

18.7

8

360

175

3.15

3.440

17.02

0

0

3

2

18.1

6

225

105

2.76

3.460

20.22

1

0

3

1

17.7 🔵 Adding/modifying table content

17.7.1 Add table title with bold part

From SO 2020-08-23: https://stackoverflow.com/questions/63530204/is-there-a-way-to-bold-part-of-a-character-string-being-passed-to-add-header-lin?noredirect=1#comment112346997_63530204

mtcars_ft <- flextable(head(mtcars)) %>% 
  # Add a blank title line to top of table
  add_header_lines("") %>% 
  # Use compose to bold "Table #."
  compose(
    i = 1, part = "header",
    value = as_paragraph(
      as_chunk("Table 1. ", props = fp_text(bold = TRUE)),
      "Here is my example mtcars ft."
    ),
  )

mtcars_ft

Table 1. Here is my example mtcars ft.

mpg

cyl

disp

hp

drat

wt

qsec

vs

am

gear

carb

21.0

6

160

110

3.90

2.620

16.46

0

1

4

4

21.0

6

160

110

3.90

2.875

17.02

0

1

4

4

22.8

4

108

93

3.85

2.320

18.61

1

1

4

1

21.4

6

258

110

3.08

3.215

19.44

1

0

3

1

18.7

8

360

175

3.15

3.440

17.02

0

0

3

2

18.1

6

225

105

2.76

3.460

20.22

1

0

3

1

17.7.2 Adding blank rows

I created a post about this on StackOverflow.

When creating tables in Word reports, I often want to add blank rows in between variables. As a trivial toy example:

doc <- read_docx()
table_no_breaks <- mtcars %>% 
  count(cyl)
table_no_breaks
##   cyl  n
## 1   4 11
## 2   6  7
## 3   8 14
table_no_breaks_ft <- flextable(table_no_breaks)
table_no_breaks_ft

cyl

n

4

11

6

7

8

14

doc <- doc %>% 
  body_add_flextable(table_no_breaks_ft) %>% 
  body_add_par("")
print(
  doc, 
  "examples/flextable_no_blank_rows.docx"
)

Results in a table that looks like this: flextable_no_blank_rows.docx

I can add line breaks directly to the data frame like this:

table_breaks <- table_no_breaks %>% 
  mutate(
    across(
      everything(),
      as.character
    )
  ) %>% 
  add_row(cyl = NA, n = NA, .after = 1) %>% 
  add_row(cyl = NA, n = NA, .after = 3) %>%
  add_row(cyl = NA, n = NA, .after = 5)

table_breaks
##    cyl    n
## 1    4   11
## 2 <NA> <NA>
## 3    6    7
## 4 <NA> <NA>
## 5    8   14
## 6 <NA> <NA>
table_breaks_ft <- flextable(table_breaks)
table_breaks_ft

cyl

n

4

11

6

7

8

14

doc <- doc %>% 
  body_add_flextable(table_breaks_ft)
print(
  doc, 
  "examples/flextable_blank_rows.docx"
)

Which results in the Word table that I want: flextable_blank_rows.docx

17.7.2.1 Using padding instead of adding blank rows

I can also use padding to create space between rows. This method works especially well when All categories of categorical variables are collapsed into a single row.

padding_example <- tribble(
  ~var, ~formatted_stats, ~Control,
  "Sex\n  Female\n  Male", "\n10 (20%)\n40 (80%)", "\n5 (21%)\n19 (79%)",
  "Married\n  No\n  Yes", "\n21 (42%)\n29 (58%)", "\n12 (50%)\n4 (50%)"
)

flextable(padding_example) %>% 
  autofit() %>% 
  padding(padding.bottom = 10, part = "body")

var

formatted_stats

Control

Sex
Female
Male


10 (20%)
40 (80%)


5 (21%)
19 (79%)

Married
No
Yes


21 (42%)
29 (58%)


12 (50%)
4 (50%)

17.7.3 Change column header text

Example from Tables chapter, which is from L2C smartphone paper.

Compose chapter in flextable book

# Calculate group n's
n_outcome <- c("No" = 53, "Yes" = 47)
# Simulate data
table <- tribble(
  ~var,                ~No,                     ~Yes,
  "age",               "34.89 (30.19 - 39.58)", "35.38 (30.58 - 40.19)",
  "",                  "",                      "",
  "age_group",         "",                      "",
  "  Younger than 30", "58.49 (44.63 - 71.12)", "61.70 (46.88 - 74.63)",
  "  30 and Older",    "41.51 (28.88 - 55.37)", "38.30 (25.37 - 53.12)"
)
flextable(table) %>% 
  width(width = c(3, 2, 2)) %>% 
  # Center the final two columns
  align(j = c(2, 3), align = "center", part = "all") %>% 
  # Change header names -- add subgroup Ns to headers
  set_header_labels(
    var = "Characteristic",
    No = paste0("No\n(n=", n_outcome["No"], ")"), 
    Yes = paste0("Yes\n(n=", n_outcome["Yes"], ")")
  ) %>% 
  # Bold column headers
  bold(part = "header") 

Characteristic

No
(n=53)

Yes
(n=47)

age

34.89 (30.19 - 39.58)

35.38 (30.58 - 40.19)

age_group

Younger than 30

58.49 (44.63 - 71.12)

61.70 (46.88 - 74.63)

30 and Older

41.51 (28.88 - 55.37)

38.30 (25.37 - 53.12)

17.7.4 Change row header text

Example from Tables chapter, which is from L2C smartphone paper.

Compose chapter in flextable book

# Simulate data
table <- tribble(
  ~var,                ~No,                     ~Yes,
  "age",               "34.89 (30.19 - 39.58)", "35.38 (30.58 - 40.19)",
  "",                  "",                      "",
  "age_group",         "",                      "",
  "  Younger than 30", "58.49 (44.63 - 71.12)", "61.70 (46.88 - 74.63)",
  "  30 and Older",    "41.51 (28.88 - 55.37)", "38.30 (25.37 - 53.12)"
)
flextable(table) %>% 
  width(width = c(3, 2, 2)) %>% 
  # Change text by location 
  compose(i = 1, j = 1, value = as_paragraph("Age, mean (95% CI)")) %>% 
  # Change text conditionally
  compose(i = ~ var == "age_group", j = 1, value = as_paragraph("Age group, row percent (95% CI)"))

var

No

Yes

Age, mean (95% CI)

34.89 (30.19 - 39.58)

35.38 (30.58 - 40.19)

Age group, row percent (95% CI)

Younger than 30

58.49 (44.63 - 71.12)

61.70 (46.88 - 74.63)

30 and Older

41.51 (28.88 - 55.37)

38.30 (25.37 - 53.12)

17.7.5 Add footnote

Example from Tables chapter, which is from L2C smartphone paper.

Footnote documentation

# Simulate data
table <- tribble(
  ~var,                ~No,                     ~Yes,
  "age",               "34.89 (30.19 - 39.58)", "35.38 (30.58 - 40.19)",
  "",                  "",                      "",
  "age_group",         "",                      "",
  "  Younger than 30", "58.49 (44.63 - 71.12)", "61.70 (46.88 - 74.63)",
  "  30 and Older",    "41.51 (28.88 - 55.37)", "38.30 (25.37 - 53.12)"
)

Add a superscript “1” behind age and a numbered footnote at the bottom of the table.

flextable(table) %>% 
  width(width = c(3, 2, 2)) %>% 
  footnote(i = 1, j = 1, value = as_paragraph("Test Footnote"), ref_symbols = "1")

var

No

Yes

age1

34.89 (30.19 - 39.58)

35.38 (30.58 - 40.19)

age_group

Younger than 30

58.49 (44.63 - 71.12)

61.70 (46.88 - 74.63)

30 and Older

41.51 (28.88 - 55.37)

38.30 (25.37 - 53.12)

1Test Footnote

Or more than one at at a time:

flextable(table) %>% 
  width(width = c(3, 2, 2)) %>% 
  footnote(
    i = c(1, 3), j = 1,
    value = as_paragraph(
      c("Age in years.", "Age grouped above and below median.")
    ),
    ref_symbols = c("1", "2")
  )

var

No

Yes

age1

34.89 (30.19 - 39.58)

35.38 (30.58 - 40.19)

age_group2

Younger than 30

58.49 (44.63 - 71.12)

61.70 (46.88 - 74.63)

30 and Older

41.51 (28.88 - 55.37)

38.30 (25.37 - 53.12)

1Age in years.

2Age grouped above and below median.

17.8 🔵 Formatting

17.8.1 Change font to TNR

flextable(head(mtcars)) %>% 
  font(fontname = "Times New Roman", part = "all")

mpg

cyl

disp

hp

drat

wt

qsec

vs

am

gear

carb

21.0

6

160

110

3.90

2.620

16.46

0

1

4

4

21.0

6

160

110

3.90

2.875

17.02

0

1

4

4

22.8

4

108

93

3.85

2.320

18.61

1

1

4

1

21.4

6

258

110

3.08

3.215

19.44

1

0

3

1

18.7

8

360

175

3.15

3.440

17.02

0

0

3

2

18.1

6

225

105

2.76

3.460

20.22

1

0

3

1

17.8.2 Conditional formatting

flextable(head(mtcars)) %>% 
  # If cyl is 4 turn all text blue
  color(i = ~ cyl == 4, color = "blue") %>% 
  # mpg is greater than average mpg then format the color to red
  color(i = ~ mpg > mean(mpg), j = "mpg", color = "red")

mpg

cyl

disp

hp

drat

wt

qsec

vs

am

gear

carb

21.0

6

160

110

3.90

2.620

16.46

0

1

4

4

21.0

6

160

110

3.90

2.875

17.02

0

1

4

4

22.8

4

108

93

3.85

2.320

18.61

1

1

4

1

21.4

6

258

110

3.08

3.215

19.44

1

0

3

1

18.7

8

360

175

3.15

3.440

17.02

0

0

3

2

18.1

6

225

105

2.76

3.460

20.22

1

0

3

1

17.9 🔵Layout

17.9.1 Autofit to contents

flextable(head(mtcars)) %>% 
  autofit()

mpg

cyl

disp

hp

drat

wt

qsec

vs

am

gear

carb

21.0

6

160

110

3.90

2.620

16.46

0

1

4

4

21.0

6

160

110

3.90

2.875

17.02

0

1

4

4

22.8

4

108

93

3.85

2.320

18.61

1

1

4

1

21.4

6

258

110

3.08

3.215

19.44

1

0

3

1

18.7

8

360

175

3.15

3.440

17.02

0

0

3

2

18.1

6

225

105

2.76

3.460

20.22

1

0

3

1

17.9.2 All categories of categorical variables are collapsed into a single row

Merge doesn’t work. Merge collapses identical values down. It won’t collapse cells with non-identical values.

Instead, you have to use paste and \n to collapse the text from multiple rows into a single character string.

# All categories in one row
# Space between categories
collapse_example <- tribble(
  ~Characteristic,         ~Overall,               ~Controls,
  "Sex\n  Female\n  Male", "\n10 (20%)\n40 (80%)", "\n5 (21%)\n19 (79%)",
  "Married\n  No\n  Yes",  "\n21 (42%)\n29 (58%)", "\n12 (50%)\n4 (50%)"
)

flextable(collapse_example) %>% 
  autofit() %>% 
  # Create vertical space between variables
  padding(padding.bottom = 10, part = "body") %>% 
  # Center column headings
  align(align = "center", part = "header") %>% 
  # Center body text
  align(j = 2:3, align = "center", part = "body")

Characteristic

Overall

Controls

Sex
Female
Male


10 (20%)
40 (80%)


5 (21%)
19 (79%)

Married
No
Yes


21 (42%)
29 (58%)


12 (50%)
4 (50%)

How do we collapse the categories into character strings? Let’s say we are starting with a data frame of formatted results like this.

collapse_example <- tribble(
  ~Characteristic, ~Overall,   ~Controls,
  "Sex",           NA,         NA,
  "  Female",      "10 (20%)", "5 (21%)",
  "  Male",        "40 (80%)", "19 (79%)"
)

collapse_one_row <- function(x) {
  # Use paste to collapse the values into a string
  x <- paste(x, collapse = "\n")
  # Remove leading NA
  x <- stringr::str_remove(x, "^NA")
  x
}

collapse_example <- collapse_example %>%
  mutate(across(.fns = collapse_one_row)) %>%
  # All rows identical now. Only keep the first one.
  slice(1)
## Warning: There was 1 warning in `mutate()`.
## ℹ In argument: `across(.fns = collapse_one_row)`.
## Caused by warning:
## ! Using `across()` without supplying `.cols` was deprecated in dplyr
##   1.1.0.
## ℹ Please supply `.cols` instead.
# Format flextable
flextable(collapse_example) %>% 
  autofit() %>% 
  # Create vertical space between variables
  padding(padding.bottom = 10, part = "body") %>% 
  # Center column headings
  align(align = "center", part = "header") %>% 
  # Center body text
  align(j = 2:3, align = "center", part = "body")

Characteristic

Overall

Controls

Sex
Female
Male


10 (20%)
40 (80%)


5 (21%)
19 (79%)

17.10 🔵 Examples

Other good examples to check out:

  • Sun Study report.
  • stroke study -> table_characteristics_by_network.Rmd.
  • L2C quarterly reports.
  • L2C paper_smartphone_app

17.10.1 LEAD panel summarize votes

Simulate data

summary_agreement_ad_abuse_type <- tibble(
  CaseID          = c("1001", "1002", "1003", "1004", "1005", "1006"), 
  physical        = rep("Agree", 6), 
  sexual          = rep("Agree", 6), 
  emotional       = c("Agree", "Disagree", "Agree", "Agree", "Agree", "Agree"),
  neglect         = rep("Agree", 6), 
  abandonment     = c("Agree", "Agree", NA, "Agree", "Agree", "Agree"), 
  financial       = rep("Agree", 6), 
  selfneglect     = rep("Agree", 6), 
  TotalAgreement  = c(TRUE, FALSE, FALSE, TRUE, TRUE, TRUE), 
  AnyDisagreement = c(FALSE, TRUE, TRUE, FALSE, FALSE, FALSE)
)

Make table

summary_agreement_ad_abuse_type_ft <- flextable(
  # Remove unneeded columns
  summary_agreement_ad_abuse_type %>% 
    select(
      CaseID, physical, sexual, emotional, neglect, 
      abandonment, financial, selfneglect
    )
  ) %>% 
  # Column width: Trial and error
  # Make a table and play with properties
  width(
    j = c(1:8), 
    width = c(0.98, 0.66, 0.56, 0.78, 0.71, 1.01, 0.71, 0.90)
  ) %>% 
  # Improve readability of column headers
  set_header_labels(CaseID = "Case Number", selfneglect = "Self Neglect") %>% 
  # Add title to top of table
  # Add a blank title line to top of table
  add_header_lines("") %>% 
  # Use compose to bold "Table #."
  compose(
    i = 1, part = "header",
    value = as_paragraph(
      as_chunk("Table 2. ", props = fp_text(bold = TRUE)),
      "Presence/absence of unanimous agreement for each abuse type by case number."
    ),
  ) %>%
  # Change font to times new roman
  font(fontname = "Times New Roman", part = "all") %>% 
  # Change background color of first column
  bg(j = 1, bg = "#E5E8E8", part = "body") %>% 
  # Center column headings
  align(i = 2, align = "center", part = "header") %>% 
  # Center body text
  align(align = "center", part = "body") %>% 
  # Conditionally format disagree to red
  color(i = ~ physical == "Disagree", j = c("CaseID", "physical"), color = "red") %>%
  color(i = ~ sexual == "Disagree", j = c("CaseID", "sexual"), color = "red") %>%
  color(i = ~ emotional == "Disagree", j = c("CaseID", "emotional"), color = "red") %>%
  color(i = ~ neglect == "Disagree", j = c("CaseID", "neglect"), color = "red") %>%
  color(i = ~ abandonment == "Disagree", j = c("CaseID", "abandonment"), color = "red") %>%
  color(i = ~ financial == "Disagree", j = c("CaseID", "financial"), color = "red") %>%
  color(i = ~ selfneglect == "Disagree", j = c("CaseID", "selfneglect"), color = "red")

# For checking
summary_agreement_ad_abuse_type_ft

Table 2. Presence/absence of unanimous agreement for each abuse type by case number.

Case Number

physical

sexual

emotional

neglect

abandonment

financial

Self Neglect

1001

Agree

Agree

Agree

Agree

Agree

Agree

Agree

1002

Agree

Agree

Disagree

Agree

Agree

Agree

Agree

1003

Agree

Agree

Agree

Agree

Agree

Agree

1004

Agree

Agree

Agree

Agree

Agree

Agree

Agree

1005

Agree

Agree

Agree

Agree

Agree

Agree

Agree

1006

Agree

Agree

Agree

Agree

Agree

Agree

Agree