Chapter 7 Layout
flextable
layout can be easily managed. A set of functions will let you merge
cells, add title rows, change the widths or heights.
7.1 Cell merging
To illustrate functions, we will use a basic flextable example:
dat <- data.frame(
letters1 = c("a", "b", "b", "c"),
letters2 = c("d", "e", "b", "b"),
number = 1:4, stringsAsFactors = FALSE )
myft <- flextable(dat)
myft <- theme_box(myft)
myft
letters1 |
letters2 |
number |
---|---|---|
a |
d |
1 |
b |
e |
2 |
b |
b |
3 |
c |
b |
4 |
7.1.1 vertical merging of similar values
merge_v()
will merge adjacent duplicated cells for each column of the selection.
merge_v(myft, j = ~ letters1 + letters2 )
letters1 |
letters2 |
number |
---|---|---|
a |
d |
1 |
b |
e |
2 |
b |
3 |
|
c |
4 |
7.1.2 horizontal merging of similar values
merge_h()
will merge adjacent duplicated cells for each row of the selection.
merge_h(myft)
letters1 |
letters2 |
number |
---|---|---|
a |
d |
1 |
b |
e |
2 |
b |
3 |
|
c |
b |
4 |
7.1.3 horizontal merging of columns
Function merge_h_range
is close to the previous one but merge
all colums between a range of columns.
merge_h_range(myft,
i = ~ number < 3,
j1 = "letters1", j2 = "letters2")
letters1 |
letters2 |
number |
---|---|---|
a |
1 |
|
b |
2 |
|
b |
b |
3 |
c |
b |
4 |
7.1.4 general merging function
merge_at()
will merge cells for a given continuous selection of rows and cells.
The result is a single cell.
letters1 |
letters2 |
number |
---|---|---|
a |
1 |
|
2 | ||
b |
b |
3 |
c |
b |
4 |
Note that the content that is rendered is not the result of the concatenation of each paragraphs contained into the merge cells. Only one paragraph will be kept, the top left one of all merge cells.
7.1.5 delete merging informations
If you want to get rid of all merging (i.e. for development purposes), use merge_none()
:
merge_none(myft)
letters1 |
letters2 |
number |
---|---|---|
a |
d |
1 |
b |
e |
2 |
b |
b |
3 |
c |
b |
4 |
7.1.6 Borders and merging
When cells are merged, the rendered borders will be those of the first cell. If a
column is made of three merged cells, the bottom border that will be seen will be
the bottom border of the first cell in the column. From a user point of view, this
is wrong, the bottom should be the one defined for cell 3. Function fix_border_issues
is trying to fix that issue.
ft <- data.frame(a = 1:5, b = 6:10) %>%
flextable() %>% theme_box() %>%
merge_at(i = 4:5, j = 1, part = "body") %>%
hline(i = 5, part = "body",
border = fp_border(color = "orange", width = 3) )
ft
a |
b |
---|---|
1 |
6 |
2 |
7 |
3 |
8 |
4 |
9 |
10 |
a |
b |
---|---|
1 |
6 |
2 |
7 |
3 |
8 |
4 |
9 |
10 |
7.2 Table sizes
Widths or heights must be used wisely. Each output format has its own specificities. flextable tries to make the displays identical between each format but differences may occur and sometimes need to be taken into account.
For example: :
- the PowerPoint format does not support automatic sizing (with
set_table_properties(layout = "autofit")
). - automatic sizing is preferable when the output is PDF because of the smaller font sizes and the display optimization capabilities of the PDF engine.
7.2.1 Table width
By default, table width is fixed. This setting allows to have the same rendering with Word, PDF, HTML and PowerPoint formats.
You can use another setting with function set_table_properties()
when argument
layout
is set to ‘autofit’, an algorithm implemented by HTML, PDF and Word.
When layout is set to autofit, you can also size the table along an available
width with argument width
, for example, 1 means 100% of the available width.
Note this is ignored when output is PDF. Its default value is 0, as an effect,
it only use necessary width to display all content.
ft <- qflextable(head(airquality))
set_table_properties(ft, layout = "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 |
<na> |
<na> |
14.3 |
56 |
5 |
5 |
28 |
<na> |
14.9 |
66 |
5 |
6 |
set_table_properties(ft, width = .5, layout = "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 |
<na> |
<na> |
14.3 |
56 |
5 |
5 |
28 |
<na> |
14.9 |
66 |
5 |
6 |
set_table_properties(ft, width = 1, layout = "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 |
<na> |
<na> |
14.3 |
56 |
5 |
5 |
28 |
<na> |
14.9 |
66 |
5 |
6 |
7.2.2 Cell widths and heights
This part applies only when layout is “fixed”.
The default sizes of flextable columns and rows are set by default values.
This will drive to inadequate rows heights and columns widths in some cases.
You can use function dim
to get flextable dimensions.
ft_base <- flextable(head(mtcars))
ft_base <- theme_vader(ft_base)
ft_base
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 |
dim(ft_base)
# $widths
# mpg cyl disp hp drat wt qsec vs am gear carb
# 0.75 0.75 0.75 0.75 0.75 0.75 0.75 0.75 0.75 0.75 0.75
#
# $heights
# [1] 0.25 0.25 0.25 0.25 0.25 0.25 0.25
7.2.2.1 Pretty dimensions
Function dim_pretty()
is computing optimized widths and heights.
dim_pretty(ft_base)
# $widths
# [1] 0.4811560 0.3602318 0.4640729 0.4102128 0.4728009 0.5377016 0.5377016
# [8] 0.3109967 0.3904442 0.4863779 0.4850351
#
# $heights
# [1] 0.3009259 0.2610157 0.2610157 0.2610157 0.2610157 0.2610157 0.2610157
7.2.2.2 Adjusts automatically cell widths and heights
Function autofit()
optimises widths and heights of the flextable.
This function makes compact tables.
autofit
was not my biggest naming idea as users are thinking it is the ‘Microsoft Word feature’ (see set_table_properties)
# $widths
# mpg cyl disp hp drat wt qsec vs
# 0.4811560 0.3602318 0.4640729 0.4102128 0.4728009 0.5377016 0.5377016 0.3109967
# am gear carb
# 0.3904442 0.4863779 0.4850351
#
# $heights
# [1] 0.3009259 0.2610157 0.2610157 0.2610157 0.2610157 0.2610157 0.2610157
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 |
Soft returns (a line break in a paragraph) support : function
autofit
anddim_pretty
do not support soft returns and may return wrong results (backslash n will be considered as ““).
7.2.2.3 Adjusts manually cell widths and heights
Function width()
and height()
let you control dimensions of a flextable.
height_all()
is an helper function to set the same height to each part of the
table.
Function height()
has no effect when the rule for line height is set to
“auto”, which is the default case, except with PowerPoint which does not support
this automatic line height adjustment feature. You can define that rule with function
hrule
.
Function hrule
support three options :
- “atleast”: height should be at least the value specified,
- “exact” : height should be exactly the value specified,
- “auto”, the default value : height is determined based on the height of the contents, so the value is ignored.
The following illustration is using dataset “correlations” (see table 15.5).
cor_color <- function(x){
col_palette <- c("#D73027", "#F46D43", "#FDAE61", "#FEE08B",
"#D9EF8B", "#A6D96A", "#66BD63", "#1A9850")
mycut <- cut(x,
breaks = c(-1, -0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75, 1),
include.lowest = TRUE, label = FALSE)
col_palette[mycut]
}
std_border <- fp_border(color = "white")
ft <- flextable(correlations) %>%
border_outer(part="all", border = std_border ) %>%
border_inner(border = std_border, part="all") %>%
compose(i = 1, j = 1, value = as_paragraph(""), part = "header") %>%
compose(j = ~ . -rowname, value = as_paragraph(""), part = "body") %>%
bg(j = ~ . -rowname, bg = cor_color)
ft
mpg |
cyl |
disp |
hp |
drat |
wt |
qsec |
vs |
am |
gear |
carb |
|
---|---|---|---|---|---|---|---|---|---|---|---|
mpg |
|||||||||||
cyl |
|||||||||||
disp |
|||||||||||
hp |
|||||||||||
drat |
|||||||||||
wt |
|||||||||||
qsec |
|||||||||||
vs |
|||||||||||
am |
|||||||||||
gear |
|||||||||||
carb |
To give a more regular appearance, all cells will be sized to 0.5 inches in width and height.
ft <- height(ft, height = .5) %>%
hrule(rule = "exact", part = "body") %>%
width(width = .5) %>%
set_table_properties(layout = "fixed")
ft
mpg |
cyl |
disp |
hp |
drat |
wt |
qsec |
vs |
am |
gear |
carb |
|
---|---|---|---|---|---|---|---|---|---|---|---|
mpg |
|||||||||||
cyl |
|||||||||||
disp |
|||||||||||
hp |
|||||||||||
drat |
|||||||||||
wt |
|||||||||||
qsec |
|||||||||||
vs |
|||||||||||
am |
|||||||||||
gear |
|||||||||||
carb |
7.3 Pagination
The pagination of tables allows you to control their position in relation to page breaks.
When working with Word or RTF, it is possible to prevents breaks between
tables rows you want to stay together. Function paginate()
let you
define this pagination.
You probably want to display small tables on a single page, with
no page break; in that case, use paginate()
so that all rows
should be displayed together:
paginate(x, init = TRUE, hdr_ftr = TRUE)
For large tables, it is recommended to use a setting that indicates that all rows of the header should be bound to the first row of the table to avoid the case where the header is displayed alone at the bottom of the page and then repeated on the next one:
paginate(x, init = FALSE, hdr_ftr = TRUE)
For tables that present groups that you don’t want to be presented on two pages, you must use a parameterization involving the notion of group and an algorithm for determining the groups.
paginate(x, group = "grp", group_def = "rle")
The following example shows how to make sure some groups are never displayed on two pages with Word:
data.frame(group = rep(c("A", "B", "C", "D", "E", "F", "G"), each = 5),
value = rnorm(5*7)) |>
flextable() |>
colformat_double() |>
paginate(group = "group") |>
save_as_docx(path = "reports/example_paginated.docx")
library(tables)
dat <- ggplot2::diamonds
set_flextable_defaults(theme_fun = theme_vanilla)
ft_2 <- tabular(
cut * clarity ~ Format(digits = 2) *
(x + y) * (mean + sd),
data = dat
) |>
as_flextable(
spread_first_col = TRUE,
row_title = as_paragraph(
colorize("clarity: ", color = "#666666"),
colorize(as_b(.row_title), color = "#666666")
)
) |> set_caption(caption = "test") |>
paginate(init = TRUE, group = "COL1", group_def = "nonempty")
text <- "Lorem ipsum dolor sit amet, scelerisque cras, amet dui eros ultricies leo amet. \
Risus urna sociis class risus. Vitae sit natoque nisi ipsum. Mauris at volutpat tempor, \
orci vivamus. Libero ullamcorper urna dis praesent blandit risus eget! Tempus eget enim in, \
proin est tempor, eget lorem. Orci suspendisse lobortis purus aptent nec nulla tincidunt \
vitae ipsum facilisis lacinia semper primis. Aptent at praesent eu tincidunt, vel neque, mus \
cursus dictum at. Dictumst fringilla hac leo sed, vitae erat litora! Mattis in convallis \
sed ex, magna commodo nulla? Sed sapien bibendum. Congue sed nascetur euismod mollis egestas \
efficitur erat magnis, ligula ornare ac."
read_docx() |>
body_add_par(text) |>
body_add_par(text) |>
body_add_flextable(ft_2) |>
print(target = "reports/example_tabular_paginated.docx")
The call paginate(init = TRUE, group = "COL1", group_def = "nonempty")
can be
read as:
paginate the table, make sure that if possible groups defined by new nonempty values in column “COL1” 2 are displayed on the same page. Option
group_def = "nonempty"
has been written to handletables::tabular()
results.
Most often, the default group_def='rle'
will be necessary.
Runs of equal values are used to define the groups, this is
to be used with tabulator()
outputs.
library(dplyr)
dat <- ggplot2::diamonds |>
filter(clarity %in% c("I1", "SI1", "VS2")) |>
group_by(cut, color, clarity) |>
summarise(
across(.cols = all_of(c("z", "y")),
.fns = list(
mean = ~ mean(.x, na.rm = TRUE),
sd = ~ sd(.x, na.rm = TRUE)
)), .groups = "drop")
tab <- tabulator(
x = dat, rows = c("cut", "color"),
columns = "clarity",
`z stats` = as_paragraph(as_chunk(fmt_avg_dev(z_mean, z_sd))),
`y stats` = as_paragraph(as_chunk(fmt_avg_dev(y_mean, y_sd)))
)
ft_3 <- as_flextable(tab)
ft_3
cut |
color |
I1 |
SI1 |
VS2 |
||||||
---|---|---|---|---|---|---|---|---|---|---|
z stats |
y stats |
z stats |
y stats |
z stats |
y stats |
|||||
Fair |
D |
4.9 (1.0) |
7.4 (1.4) |
3.9 (0.4) |
6.0 (0.7) |
3.7 (0.5) |
5.9 (0.7) |
|||
E |
4.0 (0.4) |
6.1 (0.4) |
3.7 (0.5) |
5.9 (0.8) |
3.5 (0.5) |
5.4 (0.8) |
||||
F |
4.0 (0.7) |
6.0 (1.1) |
3.7 (0.5) |
5.9 (0.8) |
3.6 (0.5) |
5.6 (0.7) |
||||
G |
4.2 (0.7) |
6.4 (1.1) |
3.9 (0.4) |
5.9 (0.6) |
4.0 (0.5) |
6.1 (0.8) |
||||
H |
4.5 (0.7) |
6.9 (1.0) |
4.1 (0.5) |
6.4 (0.8) |
4.0 (0.6) |
6.2 (0.9) |
||||
I |
4.4 (0.5) |
6.7 (0.8) |
4.1 (0.4) |
6.4 (0.7) |
3.9 (0.5) |
6.2 (0.7) |
||||
J |
5.0 (0.9) |
7.5 (1.4) |
4.1 (0.5) |
6.5 (0.9) |
4.0 (0.6) |
6.2 (1.0) |
||||
Good |
D |
3.8 (0.7) |
6.3 (1.3) |
3.5 (0.5) |
5.5 (0.9) |
3.5 (0.5) |
5.5 (0.8) |
|||
E |
4.3 (0.6) |
6.9 (0.8) |
3.5 (0.6) |
5.6 (0.9) |
3.5 (0.5) |
5.7 (0.8) |
||||
F |
3.9 (0.6) |
6.1 (1.0) |
3.6 (0.5) |
5.7 (0.8) |
3.5 (0.6) |
5.7 (0.9) |
||||
G |
3.9 (1.1) |
6.6 (1.0) |
3.7 (0.6) |
6.0 (0.9) |
3.6 (0.6) |
5.8 (0.9) |
||||
H |
4.2 (0.6) |
6.7 (0.8) |
3.7 (0.6) |
6.0 (1.1) |
3.7 (0.7) |
5.9 (1.1) |
||||
I |
4.3 (0.6) |
7.0 (1.0) |
3.9 (0.7) |
6.1 (1.2) |
4.0 (0.7) |
6.5 (1.1) |
||||
J |
4.3 (0.6) |
7.0 (0.7) |
4.0 (0.7) |
6.4 (1.1) |
4.0 (0.6) |
6.4 (1.1) |
||||
Very Good |
D |
3.9 (0.3) |
6.2 (0.4) |
3.4 (0.5) |
5.6 (0.9) |
3.3 (0.5) |
5.4 (0.8) |
|||
E |
4.0 (0.6) |
6.4 (0.9) |
3.5 (0.5) |
5.6 (0.9) |
3.4 (0.6) |
5.5 (0.9) |
||||
F |
4.1 (0.5) |
6.7 (1.0) |
3.6 (0.5) |
5.8 (0.8) |
3.5 (0.6) |
5.7 (0.9) |
||||
G |
4.0 (0.5) |
6.5 (0.8) |
3.6 (0.6) |
5.8 (0.9) |
3.6 (0.6) |
5.8 (1.0) |
||||
H |
4.6 (0.6) |
7.4 (0.9) |
3.8 (0.7) |
6.2 (1.1) |
3.7 (0.7) |
6.0 (1.1) |
||||
I |
4.6 (0.9) |
7.5 (1.4) |
3.9 (0.7) |
6.4 (1.1) |
3.9 (0.7) |
6.4 (1.1) |
||||
J |
4.4 (0.5) |
7.2 (0.8) |
4.0 (0.7) |
6.5 (1.1) |
4.0 (0.7) |
6.5 (1.1) |
||||
Premium |
D |
4.1 (0.2) |
6.7 (0.3) |
3.4 (0.6) |
5.5 (0.9) |
3.2 (0.5) |
5.2 (0.9) |
|||
E |
3.9 (0.6) |
6.3 (1.0) |
3.4 (0.6) |
5.6 (0.9) |
3.3 (0.6) |
5.3 (0.9) |
||||
F |
4.0 (0.6) |
6.5 (1.1) |
3.6 (0.6) |
5.9 (0.9) |
3.4 (0.7) |
5.6 (1.0) |
||||
G |
4.1 (0.9) |
6.8 (1.0) |
3.7 (0.6) |
6.0 (1.0) |
3.5 (0.7) |
5.8 (1.1) |
||||
H |
4.2 (0.9) |
6.9 (1.0) |
3.9 (0.7) |
6.4 (1.1) |
3.8 (0.7) |
6.2 (1.1) |
||||
I |
4.5 (0.7) |
7.3 (1.2) |
4.0 (0.8) |
6.6 (1.2) |
4.1 (0.7) |
6.7 (1.2) |
||||
J |
4.4 (0.8) |
7.2 (1.2) |
4.1 (0.7) |
6.7 (1.1) |
4.1 (0.8) |
6.7 (1.3) |
||||
Ideal |
D |
3.7 (0.9) |
6.0 (1.4) |
3.3 (0.5) |
5.3 (0.8) |
3.1 (0.4) |
5.0 (0.7) |
|||
E |
3.9 (0.7) |
6.3 (1.1) |
3.4 (0.5) |
5.5 (0.8) |
3.1 (0.4) |
5.1 (0.7) |
||||
F |
4.0 (0.5) |
6.6 (0.8) |
3.5 (0.6) |
5.8 (0.9) |
3.3 (0.6) |
5.3 (1.0) |
||||
G |
4.1 (0.3) |
6.7 (0.5) |
3.5 (0.6) |
5.7 (1.0) |
3.5 (0.7) |
5.7 (1.1) |
||||
H |
4.5 (0.5) |
7.2 (0.8) |
3.8 (0.7) |
6.1 (1.1) |
3.5 (0.7) |
5.7 (1.1) |
||||
I |
4.3 (0.5) |
6.9 (0.7) |
3.9 (0.7) |
6.3 (1.2) |
3.7 (0.7) |
6.0 (1.2) |
||||
J |
4.9 (1.4) |
7.6 (1.9) |
4.1 (0.7) |
6.6 (1.0) |
3.9 (0.7) |
6.3 (1.2) |
The table is too long and won’t fit in a single page. We are going to use
paginate()
and use the values of first column named cut
for group
identification. In that case, groups are defined by identical successive values.
ft_3 <- paginate(ft_3, group = "cut", group_def = "rle")
read_docx() |>
body_add_flextable(ft_3) |>
print(target = "reports/example_tabulator_paginated.docx")
flextable create a flextable from tabular with columns keys named as COL1, COL2, COL3, …↩︎