1  Introduction to flextable

1.1 Tabular Reporting with flextable

The flextable package simplifies table creation and customization by providing a flexible and user-friendly interface within the R environment. The package offers numerous features for preparing table structure, formatting content, and styling appearance.

Code
library(flextable)
ft <- summarizor(cars) |>
  as_flextable(sep_w = 0) |>
  color(
    i = ~ stat == "range",
    color = "pink"
  ) |>
  bold(j = 1) |>
  italic(j = 2, italic = TRUE)
ft

Statistic
(N=50)

speed

Mean (SD)

15.40 (5.29)

Median (IQR)

15.00 (7.00)

Range

4.00 - 25.00

dist

Mean (SD)

42.98 (25.77)

Median (IQR)

36.00 (30.00)

Range

2.00 - 120.00

1.1.1 Main Functions

Two main functions are provided to create tables:

1.1.1.1 flextable

flextable() allows you to easily create a reporting table from a data.frame.

Code
head(airquality) |>
  flextable() |>
  autofit()

Ozone

Solar.R

Wind

Temp

Month

Day

41

190

7.4

67

5

1

36

118

8.0

72

5

2

12

149

12.6

74

5

3

18

313

11.5

62

5

4

14.3

56

5

5

28

14.9

66

5

6

1.1.1.2 as_flextable

The as_flextable() function is provided to transform R objects into flextable tables without having to prepare the original data.frame.

Code
ft <- with(palmerpenguins::penguins, table(species, island)) |>
  as_flextable()
ft

species

island

Biscoe

Dream

Torgersen

Total

Adelie

Count

44 (12.8%)

56 (16.3%)

52 (15.1%)

152 (44.2%)

Mar. pct (1)

26.2% ; 28.9%

45.2% ; 36.8%

100.0% ; 34.2%

Chinstrap

Count

0 (0.0%)

68 (19.8%)

0 (0.0%)

68 (19.8%)

Mar. pct

0.0% ; 0.0%

54.8% ; 100.0%

0.0% ; 0.0%

Gentoo

Count

124 (36.0%)

0 (0.0%)

0 (0.0%)

124 (36.0%)

Mar. pct

73.8% ; 100.0%

0.0% ; 0.0%

0.0% ; 0.0%

Total

Count

168 (48.8%)

124 (36.0%)

52 (15.1%)

344 (100.0%)

(1) Columns and rows percentages

Once you have a flextable object, you can use all available functions to add or remove rows and columns, color or bold cell contents, merge cells, etc.

Code
ft <- add_header_lines(ft, "Size measurements for adult foraging penguins near Palmer Station, Antarctica") |>
  italic(part = "header", i = 1) |>
  color(color = "#0099FC", part = "footer")
ft

Size measurements for adult foraging penguins near Palmer Station, Antarctica

species

island

Biscoe

Dream

Torgersen

Total

Adelie

Count

44 (12.8%)

56 (16.3%)

52 (15.1%)

152 (44.2%)

Mar. pct (1)

26.2% ; 28.9%

45.2% ; 36.8%

100.0% ; 34.2%

Chinstrap

Count

0 (0.0%)

68 (19.8%)

0 (0.0%)

68 (19.8%)

Mar. pct

0.0% ; 0.0%

54.8% ; 100.0%

0.0% ; 0.0%

Gentoo

Count

124 (36.0%)

0 (0.0%)

0 (0.0%)

124 (36.0%)

Mar. pct

73.8% ; 100.0%

0.0% ; 0.0%

0.0% ; 0.0%

Total

Count

168 (48.8%)

124 (36.0%)

52 (15.1%)

344 (100.0%)

(1) Columns and rows percentages

1.1.2 Supported Outputs

You can quickly save the result:

Code
save_as_docx(ft, path = "output/ft.docx")
save_as_pptx(ft, path = "output/ft.pptx")
save_as_image(ft, path = "output/ft.png")

You can also use R Markdown and Quarto; tables will be automatically generated upon printing.

In interactive mode, you can also call the print() function with the preview="docx" or preview="pdf" argument. The table will then be integrated into a document, which will automatically open with the associated program.

Code
print(ft, preview = "docx")

1.1.3 Creating Consistent Tables

When creating a table, default values are used for font family, font size, padding, text alignment, etc. These properties are used when creating the table and by theme functions.

It’s recommended to specify them only once in your R session to obtain consistent tables and avoid having to call the same functions repeatedly.

They can be read with the get_flextable_defaults() function and, more importantly, updated with the set_flextable_defaults() function.

Code
set_flextable_defaults(
  font.color = "#0099FC",
  border.color = "red",
  theme_fun = "theme_box"
)

dat <- data.frame(
  wool = c("A", "B"),
  L = c(44.56, 28.22),
  M = c(24, 28.77),
  H = c(24.56, 18.78)
)
flextable(dat)

wool

L

M

H

A

44.56

24.00

24.56

B

28.22

28.77

18.78

The values defined above are a bit garish. Let’s use more reasonable values. This configuration only needs to be done once in your R session.

Code
set_flextable_defaults(
  font.size = 12, font.family = "Open Sans",
  font.color = "#333333",
  table.layout = "fixed",
  border.color = "gray",
  theme_fun = theme_booktabs,
  padding.top = 3, padding.bottom = 3,
  padding.left = 4, padding.right = 4
)
Code

wool

L

M

H

A

44.56

24.00

24.56

B

28.22

28.77

18.78

1.2 Column Width

By default, flextable column widths are fixed, meaning they have a constant width independent of content. In many situations, it’s preferable to adjust column widths based on actual content. This is where the autofit() function comes in.

The autofit() function automatically adjusts column widths based on the content present in each column. Using this function, flextable columns will adjust to fit the widest content in each column. This ensures the final table is aesthetically pleasing and all data is displayed correctly without truncation.

Code
flextable(dat) |> autofit()

wool

L

M

H

A

44.56

24.00

24.56

B

28.22

28.77

18.78

It’s also possible to define automatic adjustment managed by the document rendering engine! This means letting Word, the web browser, or PDF reader freely define column sizes to optimize table display in the document. To use this option, set the table property layout='autofit' with the set_table_properties() function:

Code
flextable(dat) |> set_table_properties(layout = "autofit")

wool

L

M

H

A

44.56

24.00

24.56

B

28.22

28.77

18.78

By default, this layout is set to fixed, in which case only the autofit() function will have an effect. When the layout is set to autofit, column sizes won’t be written to the output (note: PPTX output doesn’t support ‘autofit’ layout, only ‘fixed’).

Going forward, we’ll most often use autofit().

1.3 Formatting Content

Content can be composed as the result of concatenating multiple pieces; pieces can even be images or graphics, but generally they’re text.

By default, the displayed content of each cell will be the result of simple formatting; the format() function is called for this display. The goal is to provide roughly the same display as in your R console.

1.3.1 colformat Functions

However, it’s common to need specific formatting.

Unless creating composite content, the colformat_*() functions will suffice. If it’s a character string column, it will remain as is; if it’s numbers, it will be transformed into a character string with a number of decimal places; if it’s a date, it will be transformed into a character string representing a date, and so on. You can control these options with the colformat_double(), colformat_int(), colformat_char(), colformat_date(), etc. functions.

The main parameters of these functions can be set with set_flextable_defaults(); this is even encouraged to reduce code and standardize outputs:

Code
set_flextable_defaults(decimal.mark = ",", digits = 3, big.mark = " ")

We’ve just specified that by default the decimal separator is “,” and the number of digits after the decimal should be 3.

Code
flextable(head(ggplot2::diamonds)) |>
  colformat_double() |>
  colformat_int(j = "price", suffix = "$") |>
  autofit()

carat

cut

color

clarity

depth

table

price

x

y

z

0,230

Ideal

E

SI2

61,500

55,000

326$

3,950

3,980

2,430

0,210

Premium

E

SI1

59,800

61,000

326$

3,890

3,840

2,310

0,230

Good

E

VS1

56,900

65,000

327$

4,050

4,070

2,310

0,290

Premium

I

VS2

62,400

58,000

334$

4,200

4,230

2,630

0,310

Good

J

SI2

63,300

58,000

335$

4,340

4,350

2,750

0,240

Very Good

J

VVS2

62,800

57,000

336$

3,940

3,960

2,480

Of course, it’s always possible to specify a different value:

Code
flextable(head(cars)) |>
  colformat_double(digits = 0) |>
  autofit()

speed

dist

4

2

4

10

7

4

7

22

8

16

9

10

1.3.1.1 colformat Parameters

As you may have noticed in the previous example, you can use the prefix concept. Other parameters are available:

  • prefix and suffix allow you to specify a prefix and/or suffix to use.
  • na_str allows you to specify what value to display instead of missing values.

For numbers, you can generally specify:

  • big.mark, the thousands separator
  • decimal.mark, the decimal separator
  • digits, the number of digits after the decimal point.
Code
ft <- flextable(head(airquality))
ft <- colformat_int(
  x = ft,
  na_str = "N/A"
)
autofit(ft)

Ozone

Solar.R

Wind

Temp

Month

Day

41

190

7,4

67

5

1

36

118

8,0

72

5

2

12

149

12,6

74

5

3

18

313

11,5

62

5

4

N/A

N/A

14,3

56

5

5

28

N/A

14,9

66

5

6

It’s never necessary to change your data to pre-formatted character strings; it’s strongly recommended to use these parameters instead. This way, you retain the ability to do conditional formatting on numeric values.

1.3.2 Tabs and Line Breaks

When working with flextable, if a character string contains \n it will be treated as a line break (not a new paragraph!). If a character string contains \t, it will be treated as a tab.

Code
data.frame(
  `co\nco` = paste0(c("\t", "\t\t", "\t\t\t"), 1:3),
  check.names = FALSE
) |> flextable()

co
co

1

  2

   3

We don’t recommend modifying your data to contain \n or \t. We recommend using mk_par(), prepend_chunks(), or append_chunks() instead.

Code
adsl <- dplyr::select(formatters::ex_adsl, AGE, SEX, COUNTRY, ARM)

ft <- summarizor(adsl, by = "ARM") |>
  as_flextable(
    sep_w = 0, separate_with = "variable",
    spread_first_col = TRUE
  ) |>
  align(i = ~ !is.na(variable), align = "left")
ft

A: Drug X
(N=134)

B: Placebo
(N=134)

C: Combination
(N=132)

AGE

Mean (SD)

33,769 (6,553)

35,433 (7,895)

35,432 (7,722)

Median (IQR)

33,000 (11,000)

35,000 (10,000)

35,000 (10,000)

Range

21,000 - 50,000

21,000 - 62,000

20,000 - 69,000

SEX

F

79 (59,0%)

77 (57,5%)

66 (50,0%)

M

51 (38,1%)

55 (41,0%)

60 (45,5%)

U

3 (2,2%)

2 (1,5%)

4 (3,0%)

UNDIFFERENTIATED

1 (0,7%)

0 (0,0%)

2 (1,5%)

COUNTRY

CHN

74 (55,2%)

81 (60,4%)

64 (48,5%)

USA

10 (7,5%)

13 (9,7%)

17 (12,9%)

BRA

13 (9,7%)

7 (5,2%)

10 (7,6%)

PAK

12 (9,0%)

9 (6,7%)

10 (7,6%)

NGA

8 (6,0%)

7 (5,2%)

11 (8,3%)

RUS

5 (3,7%)

8 (6,0%)

6 (4,5%)

JPN

5 (3,7%)

4 (3,0%)

9 (6,8%)

GBR

4 (3,0%)

3 (2,2%)

2 (1,5%)

CAN

3 (2,2%)

2 (1,5%)

3 (2,3%)

CHE

0 (0,0%)

0 (0,0%)

0 (0,0%)

For example, here’s how to use prepend_chunks() to add a tab before values where variable is missing, representing a subcategory.

Code
prepend_chunks(ft, i = ~ is.na(variable), j = "stat", as_chunk("\t"))

A: Drug X
(N=134)

B: Placebo
(N=134)

C: Combination
(N=132)

AGE

Mean (SD)

33,769 (6,553)

35,433 (7,895)

35,432 (7,722)

Median (IQR)

33,000 (11,000)

35,000 (10,000)

35,000 (10,000)

Range

21,000 - 50,000

21,000 - 62,000

20,000 - 69,000

SEX

F

79 (59,0%)

77 (57,5%)

66 (50,0%)

M

51 (38,1%)

55 (41,0%)

60 (45,5%)

U

3 (2,2%)

2 (1,5%)

4 (3,0%)

UNDIFFERENTIATED

1 (0,7%)

0 (0,0%)

2 (1,5%)

COUNTRY

CHN

74 (55,2%)

81 (60,4%)

64 (48,5%)

USA

10 (7,5%)

13 (9,7%)

17 (12,9%)

BRA

13 (9,7%)

7 (5,2%)

10 (7,6%)

PAK

12 (9,0%)

9 (6,7%)

10 (7,6%)

NGA

8 (6,0%)

7 (5,2%)

11 (8,3%)

RUS

5 (3,7%)

8 (6,0%)

6 (4,5%)

JPN

5 (3,7%)

4 (3,0%)

9 (6,8%)

GBR

4 (3,0%)

3 (2,2%)

2 (1,5%)

CAN

3 (2,2%)

2 (1,5%)

3 (2,3%)

CHE

0 (0,0%)

0 (0,0%)

0 (0,0%)

1.3.3 Replacing Displayed Text

The labelizor() function allows you to replace displayed values in a table with other text. You can either use text associated with the name corresponding to occurrences to replace, or use a function.

Let’s illustrate these two options with a table representing an aggregation. Starting with a simple aggregation table:

Code
library(palmerpenguins)

dat <- penguins |>
  select(species, island, ends_with("mm")) |>
  group_by(species, island) |>
  summarise(
    across(
      where(is.numeric),
      .fns = list(
        avg = ~ mean(.x, na.rm = TRUE),
        sd = ~ sd(.x, na.rm = TRUE)
      )
    ),
    .groups = "drop"
  ) |>
  rename_with(~ tolower(gsub("_mm_", "_", .x, fixed = TRUE)))

ft_pen <- flextable(dat) |>
  colformat_double() |>
  separate_header() |>
  theme_vanilla() |>
  align(align = "center", part = "all") |>
  valign(valign = "center", part = "header") |>
  autofit()
ft_pen

species

island

bill

flipper

length

depth

length

avg

sd

avg

sd

avg

sd

Adelie

Biscoe

38,975

2,481

18,370

1,189

188,795

6,729

Adelie

Dream

38,502

2,465

18,252

1,134

189,732

6,585

Adelie

Torgersen

38,951

3,025

18,429

1,339

191,196

6,232

Chinstrap

Dream

48,834

3,339

18,421

1,135

195,824

7,132

Gentoo

Biscoe

47,505

3,082

14,982

0,981

217,187

6,485

First, let’s replace the column names “avg” and “sd” with the words “Mean” and “Standard Deviation”.

Code
ft_pen <- labelizor(
  x = ft_pen,
  part = "header",
  labels = c("avg" = "Mean", "sd" = "Standard Deviation")
)
ft_pen

species

island

bill

flipper

length

depth

length

Mean

Standard Deviation

Mean

Standard Deviation

Mean

Standard Deviation

Adelie

Biscoe

38,975

2,481

18,370

1,189

188,795

6,729

Adelie

Dream

38,502

2,465

18,252

1,134

189,732

6,585

Adelie

Torgersen

38,951

3,025

18,429

1,339

191,196

6,232

Chinstrap

Dream

48,834

3,339

18,421

1,135

195,824

7,132

Gentoo

Biscoe

47,505

3,082

14,982

0,981

217,187

6,485

And now, let’s format the headers with a capital letter for the first letter and lowercase for the others:

Code
ft_pen <- labelizor(
  x = ft_pen,
  part = "header",
  labels = stringr::str_to_title
)
ft_pen

Species

Island

Bill

Flipper

Length

Depth

Length

Mean

Standard Deviation

Mean

Standard Deviation

Mean

Standard Deviation

Adelie

Biscoe

38,975

2,481

18,370

1,189

188,795

6,729

Adelie

Dream

38,502

2,465

18,252

1,134

189,732

6,585

Adelie

Torgersen

38,951

3,025

18,429

1,339

191,196

6,232

Chinstrap

Dream

48,834

3,339

18,421

1,135

195,824

7,132

Gentoo

Biscoe

47,505

3,082

14,982

0,981

217,187

6,485

1.4 Visual Characteristics

Code
dat <- data.frame(
  wool = c("A", "B"),
  L = c(44.56, 28.22),
  M = c(24, 28.77),
  H = c(24.56, 18.78)
)

1.4.1 Visual Formatting Functions

Simple functions exist to modify a formatting property:

Code
flextable(dat) |>
  fontsize(i = ~ wool %in% "A", size = 10) |>
  font(part = "all", fontname = "Inconsolata") |>
  color(part = "header", color = "#e22323", j = c("L", "M", "H")) |>
  bold(part = "header", j = c("L", "M")) |>
  italic(part = "all", j = "wool") |>
  highlight(i = ~ L < 30, color = "wheat", j = c("M", "H"))

wool

L

M

H

A

44,56

24,00

24,56

B

28,22

28,77

18,78

Code
ft <- flextable(dat) |>
  align(align = "center", part = "all") |>
  line_spacing(space = 2, part = "all") |>
  padding(padding = 6, part = "header")
ft

wool

L

M

H

A

44,56

24,00

24,56

B

28,22

28,77

18,78

Code
ft |>
  bg(bg = "black", part = "all") |>
  color(color = "white", part = "all") |>
  merge_at(i = 1:2, j = 1) |>
  valign(i = 1, valign = "bottom")

wool

L

M

H

A

44,56

24,00

24,56

28,22

28,77

18,78

Background colors, font colors, and highlight colors can be modified with a vector or with a function that returns a vector of color character strings (like with ‘ggplot2’ - see scales::col_numeric).

Code
myft <- as.data.frame(matrix(runif(5 * 5), ncol = 5)) |>
  flextable() |>
  colformat_double() |>
  autofit() |>
  align(align = "center", part = "all") |>
  bg(bg = "black", part = "header") |>
  color(color = "white", part = "all") |>
  bg(bg = scales::col_numeric(palette = "viridis", domain = c(0, 1)))
myft

V1

V2

V3

V4

V5

0,191

0,830

0,344

0,872

0,250

0,863

0,072

0,924

0,801

0,289

0,547

0,390

0,561

0,931

0,854

0,573

0,408

0,664

0,545

0,840

0,515

0,868

0,439

0,020

0,353

Cell text rotation is another option that can be used - it’s recommended to use it with hrule(rule = "exact") when the output is Word or PowerPoint.

Code
myft <- myft |>
  rotate(rotation = "tbrl", part = "header", align = "center") |>
  height(height = 1, unit = "cm", part = "header") |>
  hrule(rule = "exact", part = "header") |>
  align(align = "right", part = "header")
myft

V1

V2

V3

V4

V5

0,191

0,830

0,344

0,872

0,250

0,863

0,072

0,924

0,801

0,289

0,547

0,390

0,561

0,931

0,854

0,573

0,408

0,664

0,545

0,840

0,515

0,868

0,439

0,020

0,353

1.4.2 Borders

1.4.2.1 Inner and Outer Borders

If no conditional formatting is required, use border_outer(), border_inner_h(), and border_inner_v(). These are the simplest functions and satisfy most use cases.

Code
library(officer)
big_border <- fp_border(color = "red", width = 2)
small_border <- fp_border(color = "gray", width = 1)

myft <- flextable(head(airquality))
myft <- border_remove(x = myft)
myft <- border_outer(myft, part = "all", border = big_border)
myft <- border_inner_h(myft, part = "all", border = small_border)
myft <- border_inner_v(myft, part = "all", border = small_border)
myft

Ozone

Solar.R

Wind

Temp

Month

Day

41

190

7,4

67

5

1

36

118

8,0

72

5

2

12

149

12,6

74

5

3

18

313

11,5

62

5

4

14,3

56

5

5

28

14,9

66

5

6

1.4.2.2 Adding Lines

When greater control over border formatting is needed, the following functions can be used to add vertical or horizontal lines as borders:

Code
myft2 <- border_remove(myft)

myft2 <- vline(myft2, border = small_border, part = "all")
myft2 <- vline_left(myft2, border = big_border, part = "all")
myft2 <- vline_right(myft2, border = big_border, part = "all")
myft2 <- hline(myft2, border = small_border)
myft2 <- hline_bottom(myft2, border = big_border)
myft2 <- hline_top(myft2, border = big_border, part = "all")
myft2

Ozone

Solar.R

Wind

Temp

Month

Day

41

190

7,4

67

5

1

36

118

8,0

72

5

2

12

149

12,6

74

5

3

18

313

11,5

62

5

4

14,3

56

5

5

28

14,9

66

5

6

These functions also support row i and column j selectors.

1.4.3 Theme Functions

Theme functions are not like ‘ggplot2’ themes. They are applied to the existing table immediately. Theme functions should be applied after adding all table elements.

If you want to automatically apply a theme function to every flextable, you can use the theme_fun argument of set_flextable_defaults(); be aware that this theme function is applied as the last instruction when calling flextable() - so if you add headers or footers to the table, they won’t be formatted with the theme.

You can also use the post_process_html argument of set_flextable_defaults() (or post_process_pdf, post_process_docx, post_process_pptx) to specify a theme to apply systematically before printing flextable(). Make sure your theme doesn’t override formatting done before the print instruction.

1.4.3.1 Available Themes

Code
ft <- flextable(head(airquality))
ft <- add_header_row(ft,
  top = TRUE,
  values = c("measurements", "time"),
  colwidths = c(4, 2)
)
ft <- align(ft, i = 1, align = "center", part = "header")
ft <- width(ft, width = .75)
  • theme_booktabs() is the default theme. It adds borders, aligns text left in text columns, and aligns right in non-text columns.
Code

measurements

time

Ozone

Solar.R

Wind

Temp

Month

Day

41

190

7,4

67

5

1

36

118

8,0

72

5

2

12

149

12,6

74

5

3

18

313

11,5

62

5

4

14,3

56

5

5

28

14,9

66

5

6

Code

measurements

time

Ozone

Solar.R

Wind

Temp

Month

Day

41

190

7,4

67

5

1

36

118

8,0

72

5

2

12

149

12,6

74

5

3

18

313

11,5

62

5

4

14,3

56

5

5

28

14,9

66

5

6

Code

measurements

time

Ozone

Solar.R

Wind

Temp

Month

Day

41

190

7,4

67

5

1

36

118

8,0

72

5

2

12

149

12,6

74

5

3

18

313

11,5

62

5

4

14,3

56

5

5

28

14,9

66

5

6

  • theme_box() isn’t pretty but useful when creating a table if you want to ensure the layout you’re defining is exactly as expected:
Code

measurements

time

Ozone

Solar.R

Wind

Temp

Month

Day

41

190

7,4

67

5

1

36

118

8,0

72

5

2

12

149

12,6

74

5

3

18

313

11,5

62

5

4

14,3

56

5

5

28

14,9

66

5

6

Code

measurements

time

Ozone

Solar.R

Wind

Temp

Month

Day

41

190

7,4

67

5

1

36

118

8,0

72

5

2

12

149

12,6

74

5

3

18

313

11,5

62

5

4

14,3

56

5

5

28

14,9

66

5

6

1.4.3.2 Defining Your Own Theme

You can easily define your own theme by creating a function.

Code
my_theme <- function(x, ...) {
  x <- colformat_double(x, big.mark = "'", decimal.mark = ",", digits = 1)
  x <- set_table_properties(x, layout = "fixed")
  x <- border_remove(x)
  std_border <- fp_border(width = 1, color = "orange")
  x <- border_outer(x, part = "all", border = std_border)
  x <- border_inner_h(x, border = std_border, part = "all")
  x <- border_inner_v(x, border = std_border, part = "all")
  autofit(x)
}
my_theme(ft)

measurements

time

Ozone

Solar.R

Wind

Temp

Month

Day

41

190

7,4

67

5

1

36

118

8,0

72

5

2

12

149

12,6

74

5

3

18

313

11,5

62

5

4

14,3

56

5

5

28

14,9

66

5

6

1.5 Table Headers and Footers

Headers, footers (and body) of the table can be supplemented with rows and displayed values can be modified.

1.5.1 Separating Column Names into Multiple Lines

When column names contain multiple labels (resulting from concatenation, for example), they can be distributed and organized across multiple lines very simply using the separate_header() function.

Code
ft <- flextable(head(iris))
separate_header(ft)

Sepal

Petal

Species

Length

Width

Length

Width

5,1

3,5

1,4

0,2

setosa

4,9

3,0

1,4

0,2

setosa

4,7

3,2

1,3

0,2

setosa

4,6

3,1

1,5

0,2

setosa

5,0

3,6

1,4

0,2

setosa

5,4

3,9

1,7

0,4

setosa

This is particularly useful for presenting aggregations performed with the dplyr::summarise() function.

Code
library(palmerpenguins)

dat <- penguins |>
  select(species, island, ends_with("mm")) |>
  group_by(species, island) |>
  summarise(
    across(
      where(is.numeric),
      .fns = list(
        avg = ~ mean(.x, na.rm = TRUE),
        sd = ~ sd(.x, na.rm = TRUE)
      )
    ),
    .groups = "drop"
  )
dat
# A tibble: 5 × 8
  species   island    bill_length_mm_avg bill_length_mm_sd bill_depth_mm_avg
  <fct>     <fct>                  <dbl>             <dbl>             <dbl>
1 Adelie    Biscoe                  39.0              2.48              18.4
2 Adelie    Dream                   38.5              2.47              18.3
3 Adelie    Torgersen               39.0              3.03              18.4
4 Chinstrap Dream                   48.8              3.34              18.4
5 Gentoo    Biscoe                  47.5              3.08              15.0
# ℹ 3 more variables: bill_depth_mm_sd <dbl>, flipper_length_mm_avg <dbl>,
#   flipper_length_mm_sd <dbl>
Code
ft_pen <- flextable(dat) |>
  separate_header() |>
  align(align = "center", part = "all") |>
  theme_box() |>
  colformat_double(digits = 2) |>
  autofit()
ft_pen

species

island

bill

flipper

length

depth

length

mm

avg

sd

avg

sd

avg

sd

Adelie

Biscoe

38,98

2,48

18,37

1,19

188,80

6,73

Adelie

Dream

38,50

2,47

18,25

1,13

189,73

6,59

Adelie

Torgersen

38,95

3,03

18,43

1,34

191,20

6,23

Chinstrap

Dream

48,83

3,34

18,42

1,14

195,82

7,13

Gentoo

Biscoe

47,50

3,08

14,98

0,98

217,19

6,48

1.5.2 Modifying Header Labels

Use set_header_labels() to replace the labels in the bottom row of the header. When the table is created, their values are the column names of the data.frame.

Code
ft <- flextable(head(airquality))
ft <- set_header_labels(ft,
  Solar.R = "Solar R (lang)",
  Temp = "Temperature (degrees F)", Wind = "Wind (mph)",
  Ozone = "Ozone (ppb)"
)
ft <- set_table_properties(ft, layout = "autofit", width = .8)
ft

Ozone (ppb)

Solar R (lang)

Wind (mph)

Temperature (degrees F)

Month

Day

41

190

7,4

67

5

1

36

118

8,0

72

5

2

12

149

12,6

74

5

3

18

313

11,5

62

5

4

14,3

56

5

5

28

14,9

66

5

6