Tom
Tom

Reputation: 2341

Using add_header_row from flextable to create columns of varying widths

I have data as follows:

dat <- structure(list(rn = c("type_A", "type_B", "type_C"
), freq = list(c(0, 0, 0, 5, 7, 16, 28), c(2, 1, 0, 5, 0, 8), 
    c(0, 0, 3, 5, 12, 53, 73)), colspan = list(c(`25` = 1, `100` = 2, 
`250` = 1, `500` = 1, `1000` = 1, Infinity = 3, SUM = 1), c(`25` = 1, 
`100` = 2, `250` = 1, `500` = 1, Infinity = 4, SUM = 1), c(`25` = 1, 
`50` = 1, `100` = 1, `250` = 1, `500` = 1, Infinity = 4, SUM = 1
))), row.names = c(NA, 3L), class = "data.frame")

total_colspan = c(0, 25, 50, 100, 250, 500, 1000, 1500, 3000, "Infinity", "SUM")

      rn                   freq             colspan
1 type_A  0, 0, 0, 5, 7, 16, 28 1, 2, 1, 1, 1, 3, 1
2 type_B       2, 1, 0, 5, 0, 8    1, 2, 1, 1, 4, 1
3 type_C 0, 0, 3, 5, 12, 53, 73 1, 1, 1, 1, 1, 4, 1

I would like to create a table with varying column spans (but they all add up to 10), in an R-markdown Word document, like the table below:

enter image description here

I was advised to try flextable for this (link). I am trying to use the header options to create these varying colspan. I thought about doing something like:

dat_table <- flextable(dat)
dat_table <- lapply(dat_table, add_header_row, values = unlist(freq), colwidths = unlist(colspan))

But this is not working.

EDIT:

My second attempt:

dat <- structure(list(rn = c("type_A", "type_B", "type_C"
), freq = list(c(0, 0, 0, 5, 7, 16, 28), c(2, 1, 0, 5, 0, 8), 
    c(0, 0, 3, 5, 12, 53, 73)), colspan = list(c(1, 2, 1, 1, 1, 3, 1),  c(1, 2, 1, 1, 4, 1), c(1, 1, 1, 1,  1, 4, 1
))), row.names = c(NA, 3L), class = "data.frame")

# The thresholds as in the picture
thresholds <- data.frame(c("Lower threshold","Upper threshold"), c(0,25), c(25,50), c(50,100), c(100,250), c(250,500),c(500,1000),c(1000,1500),c(1500,3000),c(3000, "Infinity"), c("", "SUM"))
names(thresholds) <- c("One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven")
thresholds <- flextable(thresholds)

# There was one column to few in the example
dat <- transform(dat, colspan=Map('c', 1, dat[["colspan"]] ))
dat <- transform(dat, freq=Map('c', "", dat[["freq"]] ))

# for loop to stick to the syntax
for (i in nrow(dat)) {
 thresholds <- add_header_row(thresholds, values = dat[[2]][[i]], colwidths = dat[[3]][[i]])
}

For some reason it only adds one row (while it allows for more headers to be added).

enter image description here

Upvotes: 5

Views: 1471

Answers (3)

Matt
Matt

Reputation: 7385

Here's a solution that is perhaps way too overkill, but seems to do what you're looking for:

library(tidyverse)
library(flextable)

dat <- structure(list(rn = c("type_A", "type_B", "type_C"
), freq = list(c(0, 0, 0, 5, 7, 16, 28), c(2, 1, 0, 5, 0, 8), 
               c(0, 0, 3, 5, 12, 53, 73)), colspan = list(c(1, 2, 1, 1, 1, 3, 1),  c(1, 2, 1, 1, 4, 1), c(1, 1, 1, 1,  1, 4, 1
               ))), row.names = c(NA, 3L), class = "data.frame")

# The thresholds as in the picture
thresholds <- data.frame(c("Lower threshold","Upper threshold"), c(0,25), c(25,50), c(50,100), c(100,250), c(250,500),c(500,1000),c(1000,1500),c(1500,3000),c(3000, "Infinity"), c("", "SUM"))
names(thresholds) <- c("One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven")

out <- map(1:nrow(dat), function(index){
  out <- data.frame("freq" = dat$freq[[index]], 
                    "span" = dat$colspan[[index]]) %>% 
    tidyr::uncount(span, .id = 'span') %>% 
    mutate(freq = ifelse(span>1, NA, freq)) %>% 
    t %>% 
    as.data.frame() %>% 
    mutate(rn = dat$rn[[index]],
           across(everything(), ~as.character(.))) %>% 
    select(rn, everything()) %>% 
    set_names(nm = names(thresholds)) %>% 
    slice(1)
  return(out)
}) 

combined <- thresholds %>% 
  mutate(across(everything(),  ~as.character(.))) %>% 
  bind_rows(out) 

spans <- map(1:length(dat$colspan), function(index){
  spans <- dat$colspan[[index]] %>%  
    as_tibble() %>% 
    mutate(idx = row_number()) %>% 
    tidyr::uncount(value, .remove = F) %>% 
    group_by(idx) %>%
    mutate(pos = 1:n(),
           value = ifelse(pos != 1, 0, value)) %>% 
    ungroup() %>% 
    select(value) %>% 
    t
  return(append(1, spans))
})

myft <- flextable(combined) %>% 
  theme_box()

myft$body$spans$rows[3:nrow(myft$body$spans$rows),] <- matrix(unlist(spans), ncol = ncol(combined), byrow = TRUE)

myft

Created on 2022-04-29 by the reprex package (v2.0.1)

This makes the table:

Upvotes: 3

Quinten
Quinten

Reputation: 41265

It is a bit tricky to merge those two tables. This is the closest I came to reproduce your desired table. First I created your data in a suitable way:

thresholds <- data.frame(c("Lower threshold", "Upper threshold", "type_A", "type_B", "type_C"), 
                         c(0,25, 0, 2, 0), 
                         c(25,50, 0, 1, 0), 
                         c(50,100, NA, NA,3), 
                         c(100,250,0,0,5), 
                         c(250,5005,5,5,12),
                         c(500,1000,7,0,53),
                         c(1000,1500,16,NA,NA),
                         c(1500,3000,NA,NA,NA),
                         c(3000, "Infinity",NA,NA,NA), 
                         c("SUM", "SUM", 28,8,73))
names(thresholds) <- c("One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven")

Using the officer package you can give the horizontal and vertical lines different colors you want. Using the merge_at function you can merge certain cells. With the border_inner function you get borders in the table. You can use the following code:

library(officer)
std_border = fp_border(color="gray")

library(flextable)
library(dplyr)
thresholds %>%
  flextable() %>%
  merge_at(i = 3, j = 3:4, part = "body") %>% 
  merge_at(i = 4, j = 3:4, part = "body") %>% 
  merge_at(i = 3, j = 8:10, part = "body") %>%
  merge_at(i = 4, j = 7:10, part = "body") %>% 
  merge_at(i = 5, j = 7:10, part = "body") %>% 
  border_inner(border = std_border) %>%
  align(align = "left", part = "all") 

Output:

enter image description here

Upvotes: 1

JBGruber
JBGruber

Reputation: 12410

I don't think you can pass colspan options here without quite a bit of hacking. If at all possible, I would suggest adding the information which cells need to be combined manually. This is the only option, as far as I know, in flextable:

library(flextable)
library(tidyverse)

# clean up the object
dat_clean <- dat %>% 
  mutate(freq = map2(freq, colspan, ~rep(.x, .y))) %>% 
  select(-colspan) %>% 
  unnest(freq) %>% 
  group_by(rn) %>% 
  mutate(col = paste0("col_", row_number())) %>% 
  pivot_wider(names_from = col, values_from = freq)

flextable(dat_clean) %>% 
  merge_at(i = 1, j = 3:4, part = "body") %>% 
  merge_at(i = 1, j = 7:9, part = "body") %>% 
  border_inner(part="all", border = fp_border_default()) %>% 
  align(align = "center", part = "all")

enter image description here Created on 2022-04-25 by the reprex package (v2.0.1)

Upvotes: 1

Related Questions