rlistr-markdownr-flextable

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


Solution

  • 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: