Nice cross-tabulated flextable
Required packages
We will use the following packages:
library(tidyverse)
library(flextable)
library(officer)
use_df_printer()
Sample dataset
The summary of a subset of ggplot2::diamonds will be
used to define the content of the flextable.
The code below is filtering some data so that our illustration does not contain too many columns.
dat <- ggplot2::diamonds |>
filter(
cut %in% c("Good", "Very Good"),
clarity %in% c("I1", "SI1", "VS2")
) |>
mutate(price = price / 1000)
dat
carat | cut | color | clarity | depth | table | price | x | y | z |
|---|---|---|---|---|---|---|---|---|---|
numeric | ordered | ordered | ordered | numeric | numeric | numeric | numeric | numeric | numeric |
0.3 | Very Good | H | SI1 | 61.9 | 55 | 0.3 | 4.1 | 4.1 | 2.5 |
0.3 | Good | J | SI1 | 64.0 | 55 | 0.3 | 4.2 | 4.3 | 2.7 |
0.3 | Good | J | SI1 | 63.4 | 54 | 0.4 | 4.2 | 4.3 | 2.7 |
0.3 | Good | J | SI1 | 63.8 | 56 | 0.4 | 4.2 | 4.3 | 2.7 |
0.3 | Very Good | J | SI1 | 62.7 | 59 | 0.4 | 4.2 | 4.3 | 2.7 |
0.2 | Very Good | E | VS2 | 63.8 | 55 | 0.4 | 3.9 | 3.9 | 2.5 |
0.3 | Very Good | J | SI1 | 59.4 | 62 | 0.4 | 4.4 | 4.4 | 2.6 |
0.3 | Very Good | J | SI1 | 58.1 | 62 | 0.4 | 4.4 | 4.5 | 2.6 |
0.3 | Very Good | J | VS2 | 62.2 | 57 | 0.4 | 4.3 | 4.3 | 2.7 |
0.2 | Very Good | D | VS2 | 60.5 | 61 | 0.4 | 4.0 | 4.0 | 2.4 |
n: 8549 | |||||||||
Main aggregation
Now, let’s aggregate the filtered dataset and
count observations. The created dataset will
have three dimensions, cut, color and clarity
that will be used as rows or columns in the final
flextable.
summary_dat <- dat |>
group_by(cut, color, clarity) |>
summarise(
y_mean = mean(price, na.rm = TRUE),
y_sd = sd(price, na.rm = TRUE),
.groups = "drop"
)
summary_dat
cut | color | clarity | y_mean | y_sd |
|---|---|---|---|---|
ordered | ordered | ordered | numeric | numeric |
Good | D | I1 | 3.5 | 2.2 |
Good | D | SI1 | 3.0 | 2.8 |
Good | D | VS2 | 3.6 | 3.4 |
Good | E | I1 | 4.4 | 2.5 |
Good | E | SI1 | 3.2 | 3.1 |
Good | E | VS2 | 3.8 | 3.3 |
Good | F | I1 | 2.6 | 1.9 |
Good | F | SI1 | 3.3 | 3.0 |
Good | F | VS2 | 3.8 | 3.2 |
Good | G | I1 | 3.2 | 2.0 |
n: 42 | ||||
Secondary aggregations
The following counts will be used to show the counts in the rows.
cut_counts <- count(dat, cut, name = "n_cut")
cut_counts
cut | n_cut |
|---|---|
ordered | integer |
Good | 2,634 |
Very Good | 5,915 |
n: 2 | |
The following counts will be used to show the counts in the columns.
clarity_counts <- count(dat, clarity, name = "n_clarity")
clarity_counts
clarity | n_clarity |
|---|---|
ordered | integer |
I1 | 180 |
SI1 | 4,800 |
VS2 | 3,569 |
n: 3 | |
Flextable default settings
This step is not mandatory, it defines some default values for the flextable to be produced later.
# Modify flextable defaults formatting properties ----
init_flextable_defaults()
set_flextable_defaults(
theme_fun = theme_booktabs,
big.mark = " ", font.color = "#333333",
border.color = "#333333",
padding = 3,
)
Using function tabulator
Now all datasets are ready to be used, let’s call
function tabulator() that will prepare an object
ready to be sent to as_flextable().
ftd <- fp_text_default(color = "#f24f26")
# create tabulator object to be used with `as_flextable()` ----
tab <- tabulator(
x = summary_dat,
rows = c("cut", "color"),
columns = "clarity",
hidden_data = cut_counts,
row_compose = list(
cut = as_paragraph(cut, as_chunk(x = paste0("\nn = ", n_cut), props = ftd))
),
# defines the only cells to show in the result
`y stats` = as_paragraph(y_mean, " (\u00B1 ", y_sd, ")")
)
## get colkeys corresponding to multiple "y stats" ----
colkeys <- tabulator_colnames(tab, columns = "y stats")
colkeys
## [1] "I1@y stats" "SI1@y stats" "VS2@y stats"
The flextable
A flextable is produced with the following code:
ft <- as_flextable(tab, separate_with = "cut")
ft
cut | color | I1 | SI1 | VS2 | |||
|---|---|---|---|---|---|---|---|
Good | D | 3.5 (± 2.2) | 3.0 (± 2.8) | 3.6 (± 3.4) | |||
E | 4.4 (± 2.5) | 3.2 (± 3.1) | 3.8 (± 3.3) | ||||
F | 2.6 (± 1.9) | 3.3 (± 3.0) | 3.8 (± 3.2) | ||||
G | 3.2 (± 2.0) | 4.1 (± 3.9) | 4.1 (± 3.3) | ||||
H | 3.8 (± 2.2) | 4.2 (± 3.8) | 4.4 (± 4.2) | ||||
I | 4.2 (± 2.9) | 4.7 (± 4.5) | 6.0 (± 5.1) | ||||
J | 3.8 (± 2.1) | 4.6 (± 3.9) | 4.8 (± 3.6) | ||||
Very Good | D | 2.6 (± 0.8) | 3.2 (± 2.9) | 3.1 (± 3.3) | |||
E | 3.4 (± 2.2) | 3.2 (± 3.1) | 3.3 (± 3.5) | ||||
F | 4.3 (± 2.6) | 3.6 (± 3.2) | 4.0 (± 3.8) | ||||
G | 3.2 (± 2.1) | 3.5 (± 3.6) | 4.4 (± 4.0) | ||||
H | 5.3 (± 2.5) | 4.9 (± 4.4) | 4.6 (± 4.0) | ||||
I | 6.0 (± 4.9) | 5.2 (± 4.5) | 5.8 (± 5.1) | ||||
J | 4.5 (± 2.7) | 5.0 (± 4.1) | 5.3 (± 4.5) |
Customise the table
We need to add details in the columns headers.
ft <- append_chunks(ft,
j = colkeys, i = 1,
part = "header",
as_chunk(fmt_header_n(clarity_counts$n_clarity), props = ftd)
)
ft
cut | color | I1 | SI1 | VS2 | |||
|---|---|---|---|---|---|---|---|
Good | D | 3.5 (± 2.2) | 3.0 (± 2.8) | 3.6 (± 3.4) | |||
E | 4.4 (± 2.5) | 3.2 (± 3.1) | 3.8 (± 3.3) | ||||
F | 2.6 (± 1.9) | 3.3 (± 3.0) | 3.8 (± 3.2) | ||||
G | 3.2 (± 2.0) | 4.1 (± 3.9) | 4.1 (± 3.3) | ||||
H | 3.8 (± 2.2) | 4.2 (± 3.8) | 4.4 (± 4.2) | ||||
I | 4.2 (± 2.9) | 4.7 (± 4.5) | 6.0 (± 5.1) | ||||
J | 3.8 (± 2.1) | 4.6 (± 3.9) | 4.8 (± 3.6) | ||||
Very Good | D | 2.6 (± 0.8) | 3.2 (± 2.9) | 3.1 (± 3.3) | |||
E | 3.4 (± 2.2) | 3.2 (± 3.1) | 3.3 (± 3.5) | ||||
F | 4.3 (± 2.6) | 3.6 (± 3.2) | 4.0 (± 3.8) | ||||
G | 3.2 (± 2.1) | 3.5 (± 3.6) | 4.4 (± 4.0) | ||||
H | 5.3 (± 2.5) | 4.9 (± 4.4) | 4.6 (± 4.0) | ||||
I | 6.0 (± 4.9) | 5.2 (± 4.5) | 5.8 (± 5.1) | ||||
J | 4.5 (± 2.7) | 5.0 (± 4.1) | 5.3 (± 4.5) |
Add a title in the header part:
ft <- add_header_lines(ft, "Subset of original dataset")
ft
Subset of original dataset | |||||||
|---|---|---|---|---|---|---|---|
cut | color | I1 | SI1 | VS2 | |||
Good | D | 3.5 (± 2.2) | 3.0 (± 2.8) | 3.6 (± 3.4) | |||
E | 4.4 (± 2.5) | 3.2 (± 3.1) | 3.8 (± 3.3) | ||||
F | 2.6 (± 1.9) | 3.3 (± 3.0) | 3.8 (± 3.2) | ||||
G | 3.2 (± 2.0) | 4.1 (± 3.9) | 4.1 (± 3.3) | ||||
H | 3.8 (± 2.2) | 4.2 (± 3.8) | 4.4 (± 4.2) | ||||
I | 4.2 (± 2.9) | 4.7 (± 4.5) | 6.0 (± 5.1) | ||||
J | 3.8 (± 2.1) | 4.6 (± 3.9) | 4.8 (± 3.6) | ||||
Very Good | D | 2.6 (± 0.8) | 3.2 (± 2.9) | 3.1 (± 3.3) | |||
E | 3.4 (± 2.2) | 3.2 (± 3.1) | 3.3 (± 3.5) | ||||
F | 4.3 (± 2.6) | 3.6 (± 3.2) | 4.0 (± 3.8) | ||||
G | 3.2 (± 2.1) | 3.5 (± 3.6) | 4.4 (± 4.0) | ||||
H | 5.3 (± 2.5) | 4.9 (± 4.4) | 4.6 (± 4.0) | ||||
I | 6.0 (± 4.9) | 5.2 (± 4.5) | 5.8 (± 5.1) | ||||
J | 4.5 (± 2.7) | 5.0 (± 4.1) | 5.3 (± 4.5) | ||||
And add a special line so that when rendered in line the top of the flextable will indicate the page number corresponding to its position in the document.
ft <- add_header_lines(ft, "Page N°") |>
append_chunks(i = 1, part = "header", j = 1,
as_word_field(x = "Page")) |>
align(part = "header", align = "right", i = 1) |>
set_caption(caption = "Prices of over 50 000 round cut diamonds")
ft
Page N° | |||||||
|---|---|---|---|---|---|---|---|
Subset of original dataset | |||||||
cut | color | I1 | SI1 | VS2 | |||
Good | D | 3.5 (± 2.2) | 3.0 (± 2.8) | 3.6 (± 3.4) | |||
E | 4.4 (± 2.5) | 3.2 (± 3.1) | 3.8 (± 3.3) | ||||
F | 2.6 (± 1.9) | 3.3 (± 3.0) | 3.8 (± 3.2) | ||||
G | 3.2 (± 2.0) | 4.1 (± 3.9) | 4.1 (± 3.3) | ||||
H | 3.8 (± 2.2) | 4.2 (± 3.8) | 4.4 (± 4.2) | ||||
I | 4.2 (± 2.9) | 4.7 (± 4.5) | 6.0 (± 5.1) | ||||
J | 3.8 (± 2.1) | 4.6 (± 3.9) | 4.8 (± 3.6) | ||||
Very Good | D | 2.6 (± 0.8) | 3.2 (± 2.9) | 3.1 (± 3.3) | |||
E | 3.4 (± 2.2) | 3.2 (± 3.1) | 3.3 (± 3.5) | ||||
F | 4.3 (± 2.6) | 3.6 (± 3.2) | 4.0 (± 3.8) | ||||
G | 3.2 (± 2.1) | 3.5 (± 3.6) | 4.4 (± 4.0) | ||||
H | 5.3 (± 2.5) | 4.9 (± 4.4) | 4.6 (± 4.0) | ||||
I | 6.0 (± 4.9) | 5.2 (± 4.5) | 5.8 (± 5.1) | ||||
J | 4.5 (± 2.7) | 5.0 (± 4.1) | 5.3 (± 4.5) | ||||
See how it renders in Word
The following code is producing a Word document with package ‘officer’, the content is a fake content. The purpose is to show how the flextable is rendered in a Word document.
psum_txt <- "Lorem ipsum dolor sit amet, purus ut nullam nisl vehicula non ligula sem non. Egestas nascetur, eu sed nec mattis semper arcu auctor sagittis id consequat non? Facilisi vestibulum ac nec primis. Posuere sociis ligula tempor, mattis sed sed dapibus. Taciti nulla mattis aliquet dictumst, nisi aenean, pulvinar! Hendrerit porttitor quis praesent mi nisl lorem mauris ut nulla. Tincidunt in sit sit quisque id molestie. Eros, orci ligula phasellus sed erat vel vivamus penatibus aliquam, scelerisque turpis sociis erat."
landscape_two_columns <- block_section(
prop_section(
type = "continuous",
section_columns = section_columns(widths = c(3, 3))
)
)
read_docx(path = "template.docx") |>
body_add_par(value = "Lorem ipsum", style = "heading 1") |>
body_add_par(psum_txt) |>
body_add_par(value = "Tempor velit sed", style = "heading 2") |>
body_end_block_section(value = block_section(property = prop_section(type = "continuous"))) |>
body_add_par(psum_txt) |>
body_add_par(psum_txt) |>
body_end_block_section(value = landscape_two_columns) |>
body_add_par(psum_txt) |>
body_add_par(psum_txt) |>
body_add_break() |>
body_add_par(value = "Mattis potenti metus", style = "heading 2") |>
body_add_par(value = "") |>
body_add_flextable(value = ft, topcaption = FALSE, keepnext = FALSE) |>
body_add_par(psum_txt) |>
print(target = "illustration.docx")
Here is the produced Word document: illustration.docx
After updating the fields in the document, it has this rendering: