Creating a Publication-Ready Table in R with flextable

Author

Habtamu Bizuayehu

Published

March 15, 2026

Step 1: Load Required Libraries

# Clear environment and console
rm(list = ls())
# **Load required libraries**
if (!requireNamespace("pacman", quietly = TRUE)) install.packages("pacman")

pacman::p_load(
  lubridate,  # Simplifies the manipulation of dates and times in R (e.g., formatting, extracting components)
  dplyr,      # Provides tools for data manipulation, helpful for filtering and summarizing date-based data
  stringr,    # Simplifies string manipulation
  tidyr,      # Reshaping and tidying data
  zoo,        # For working with time series data, including rolling calculations and handling missing data
  tsibble,    # Provides tools for handling time series data, including date-time indexes and features for forecasting
  readr,      # For reading date-time data from CSV
  haven,      # Data from other statistical software formats (SPSS, SAS, Stata)
  forecast,   # Useful for time series forecasting, especially when working with seasonal or trend-based data
  ggplot2,    # For visualizing date-time trends in data (e.g., time series plots)
  janitor,    # Tabulation, cleaning column names, adding totals and proportions
  plotly,     # Interactive visualizations
  # Documentation/reporting
  prettydoc,      # Pretty document templates
  flexdashboard,  # Interactive dashboards
  quarto,         # For rendering and publishing documents with the Quarto framework
  yaml,           # YAML document processing
  # Tabulation
  flextable,  # For creating styled, publication-ready tables
  gt,         # Grammar of tables
  reactable,  # Interactive tables
  scales,     # For number formatting (e.g., commas)
  officer,    # For exporting tables/reports to Word or PowerPoint
  tidyverse,
  DT,
  knitr,
  kableExtra
)
setwd("D:/OneDrive/ncid/GitHub/table_automation")
load("Vaccination_uptake.RDATA")
vacc_1524 <- vacc_data |>
  mutate(across(
    c(
      age_group,
      gender,
      marital_status,
      vacc_service_year,
      vacc_quarter,
      vacc_weekday
    ),
    as.character
  ))

kable(head(vacc_1524, 200),
      caption = "First 200 Rows of vacc_1524 (Ages 15-24 Vaccination Data)",
      format = "markdown")  |>   # Or "html", "latex"
  kable_styling(bootstrap_options = c("striped", "hover")) |>
  scroll_box(height = "400px")  # Scroll for 200 rows
First 200 Rows of vacc_1524 (Ages 15-24 Vaccination Data)
category age_group gender marital_status vacc_service_year vacc_quarter vacc_weekday
Respiratory vaccines 0-19 Male Married 2019 1 Sunday
Respiratory vaccines 50-59 Female NA 2019 4 Wednesday
Respiratory vaccines 0-19 Female Married 2024 1 Sunday
Respiratory vaccines 0-19 Male Married 2024 1 Monday
Respiratory vaccines 20-29 Male Married 2021 3 Saturday
Respiratory vaccines 40-49 Male Married 2021 4 Sunday
Respiratory vaccines 20-29 Female Single/widowed 2019 3 Sunday
Respiratory vaccines 20-29 Female Married 2017 4 Wednesday
Respiratory vaccines 0-19 Male Married 2019 3 Wednesday
Respiratory vaccines 70+ Male Married 2016 3 Saturday
Respiratory vaccines 70+ Female Single/widowed 2016 4 Thursday
Respiratory vaccines 40-49 Male Married 2017 3 Tuesday
Respiratory vaccines 0-19 Male Single/widowed 2019 1 Tuesday
Respiratory vaccines 50-59 Male Single/widowed 2022 4 Tuesday
Respiratory vaccines 60-69 Female Married 2017 1 Thursday
Respiratory vaccines 40-49 Male Single/widowed 2021 1 Monday
Respiratory vaccines 20-29 Female Married 2023 4 Wednesday
Respiratory vaccines 20-29 Male Single/widowed 2016 3 Friday
Respiratory vaccines 70+ Female Divorced 2024 4 Thursday
Respiratory vaccines 30-39 Male Married 2019 1 Wednesday
Respiratory vaccines 0-19 Female Divorced 2018 1 Monday
Respiratory vaccines 0-19 Male Single/widowed 2024 3 Sunday
Respiratory vaccines 30-39 Male Divorced 2017 4 Thursday
Respiratory vaccines 40-49 Female Divorced 2023 4 Wednesday
Respiratory vaccines 50-59 Male Single/widowed 2016 2 Wednesday
Respiratory vaccines 70+ Male Single/widowed 2019 1 Friday
Respiratory vaccines 50-59 Male Married 2023 4 Tuesday
Respiratory vaccines 50-59 Male Divorced 2016 3 Monday
Respiratory vaccines 40-49 Female Single/widowed 2016 4 Saturday
Respiratory vaccines 20-29 Female Single/widowed 2019 4 Saturday
Respiratory vaccines 40-49 Male Single/widowed 2020 3 Thursday
Respiratory vaccines 30-39 Female Married 2024 2 Friday
Respiratory vaccines 20-29 Female Divorced 2020 2 Tuesday
Respiratory vaccines 0-19 Male Single/widowed 2017 3 Wednesday
Respiratory vaccines 20-29 Male Married 2023 2 Sunday
Respiratory vaccines 60-69 Female Married 2019 2 Tuesday
Respiratory vaccines 30-39 Male Married 2024 2 Sunday
Respiratory vaccines 70+ Male Single/widowed 2023 3 Sunday
Respiratory vaccines 60-69 Female Divorced 2023 4 Thursday
Respiratory vaccines 20-29 Female Married 2021 4 Monday
Respiratory vaccines 40-49 Female Married 2024 3 Thursday
Respiratory vaccines 40-49 Female NA 2020 2 Friday
Respiratory vaccines 60-69 Female Married 2016 1 Wednesday
Respiratory vaccines 20-29 Female Single/widowed 2019 2 Thursday
Respiratory vaccines 50-59 Female Single/widowed 2016 4 Thursday
Respiratory vaccines 40-49 Male NA 2016 4 Saturday
Respiratory vaccines 50-59 Female Married 2021 3 Wednesday
Respiratory vaccines 40-49 Female Divorced 2021 4 Friday
Respiratory vaccines 20-29 Male Married 2024 1 Monday
Respiratory vaccines 20-29 Male Married 2019 2 Friday
Respiratory vaccines 30-39 Female Married 2016 1 Thursday
Respiratory vaccines 70+ Male Married 2017 1 Tuesday
Respiratory vaccines 0-19 Male Divorced 2019 3 Thursday
Respiratory vaccines 50-59 Male Married 2022 3 Sunday
Respiratory vaccines 20-29 Female Married 2017 1 Monday
Respiratory vaccines 20-29 Male Married 2019 1 Wednesday
Respiratory vaccines 60-69 Female Divorced 2015 3 Monday
Respiratory vaccines 60-69 Female Divorced 2020 1 Tuesday
Respiratory vaccines 20-29 Female NA 2024 4 Wednesday
Respiratory vaccines 40-49 Male Married 2019 3 Tuesday
Respiratory vaccines 30-39 Male Married 2023 1 Friday
Respiratory vaccines 60-69 Female Married 2016 4 Monday
Respiratory vaccines 50-59 Male Married 2024 2 Friday
Respiratory vaccines 20-29 Male Single/widowed 2016 2 Wednesday
Respiratory vaccines 70+ Female Single/widowed 2021 4 Monday
Respiratory vaccines 60-69 Female Single/widowed 2015 4 Tuesday
Respiratory vaccines 20-29 Female Single/widowed 2024 4 Tuesday
Respiratory vaccines 20-29 Male Married 2017 1 Wednesday
Respiratory vaccines 50-59 Male NA 2017 3 Monday
Respiratory vaccines 0-19 Male Married 2020 2 Friday
Respiratory vaccines 0-19 Male Married 2020 4 Friday
Respiratory vaccines 70+ Female Single/widowed 2015 2 Friday
Respiratory vaccines 40-49 Female Single/widowed 2019 2 Monday
Respiratory vaccines 50-59 Female Married 2022 2 Thursday
Respiratory vaccines 20-29 Male Divorced 2018 2 Tuesday
Respiratory vaccines 0-19 Male Married 2017 1 Saturday
Respiratory vaccines 50-59 Male NA 2022 4 Saturday
Respiratory vaccines 40-49 Female NA 2021 3 Friday
Respiratory vaccines 40-49 Male Single/widowed 2022 1 Wednesday
Respiratory vaccines 0-19 Female Divorced 2020 2 Tuesday
Respiratory vaccines 30-39 Male Single/widowed 2023 2 Saturday
Respiratory vaccines 30-39 Female Married 2021 2 Friday
Respiratory vaccines 50-59 Male Single/widowed 2024 2 Sunday
Respiratory vaccines 50-59 Female Married 2016 3 Monday
Respiratory vaccines 40-49 Male NA 2017 2 Saturday
Respiratory vaccines 60-69 Male Single/widowed 2015 2 Sunday
Respiratory vaccines 50-59 Male NA 2020 4 Tuesday
Respiratory vaccines 40-49 Male Single/widowed 2019 3 Wednesday
Respiratory vaccines 60-69 Male Married 2017 2 Monday
Respiratory vaccines 20-29 Male Divorced 2020 1 Sunday
Respiratory vaccines 20-29 Male Single/widowed 2018 3 Thursday
Respiratory vaccines 0-19 Male Married 2024 2 Sunday
Respiratory vaccines 20-29 Male Divorced 2016 3 Friday
Respiratory vaccines 50-59 Female Married 2023 1 Wednesday
Respiratory vaccines 60-69 Female Divorced 2022 4 Monday
Respiratory vaccines 70+ Male Single/widowed 2022 3 Wednesday
Respiratory vaccines 50-59 Female Married 2021 2 Friday
Respiratory vaccines 40-49 Female Married 2017 1 Monday
Respiratory vaccines 0-19 Female Divorced 2016 2 Thursday
Respiratory vaccines 40-49 Male Divorced 2015 3 Friday
Respiratory vaccines 70+ Female Married 2015 2 Wednesday
Respiratory vaccines 20-29 Female Married 2018 3 Saturday
Respiratory vaccines 20-29 Female Married 2015 3 Thursday
Respiratory vaccines 30-39 Male Single/widowed 2015 2 Friday
Respiratory vaccines 50-59 Female Divorced 2024 3 Saturday
Respiratory vaccines 50-59 Male NA 2022 4 Monday
Respiratory vaccines 60-69 Female Single/widowed 2015 3 Monday
Respiratory vaccines 30-39 Male Married 2016 1 Wednesday
Respiratory vaccines 60-69 Female NA 2020 4 Friday
Respiratory vaccines 40-49 Male NA 2017 4 Friday
Respiratory vaccines 50-59 Male Divorced 2015 2 Monday
Respiratory vaccines 0-19 Male Married 2024 1 Monday
Respiratory vaccines 40-49 Female NA 2018 4 Thursday
Respiratory vaccines 0-19 Female Married 2022 3 Saturday
Respiratory vaccines 0-19 Male Married 2018 3 Wednesday
Respiratory vaccines 50-59 Male Divorced 2024 4 Saturday
Respiratory vaccines 60-69 Male Married 2022 2 Saturday
Respiratory vaccines 30-39 Male NA 2023 1 Friday
Respiratory vaccines 0-19 Male Married 2021 3 Thursday
Respiratory vaccines 20-29 Male Married 2015 4 Wednesday
Respiratory vaccines 50-59 Female Married 2019 2 Friday
Respiratory vaccines 70+ Male Married 2018 1 Sunday
Respiratory vaccines 60-69 Male Single/widowed 2015 3 Saturday
Respiratory vaccines 0-19 Female Married 2019 4 Tuesday
Respiratory vaccines 50-59 Female Married 2022 4 Monday
Respiratory vaccines 60-69 Female Divorced 2020 4 Friday
Respiratory vaccines 50-59 Male Single/widowed 2016 2 Tuesday
Respiratory vaccines 0-19 Male Divorced 2019 2 Thursday
Respiratory vaccines 70+ Female Single/widowed 2018 4 Wednesday
Respiratory vaccines 20-29 Male Married 2021 2 Thursday
Respiratory vaccines 30-39 Male Single/widowed 2022 2 Friday
Respiratory vaccines 20-29 Female Married 2020 2 Friday
Respiratory vaccines 20-29 Female Married 2018 4 Thursday
Respiratory vaccines 0-19 Female Divorced 2024 2 Friday
Respiratory vaccines 30-39 Female Married 2019 2 Friday
Respiratory vaccines 0-19 Female NA 2017 4 Tuesday
Respiratory vaccines 50-59 Female Single/widowed 2023 3 Sunday
Respiratory vaccines 0-19 Female Married 2021 4 Monday
Respiratory vaccines 30-39 Female Married 2017 2 Sunday
Respiratory vaccines 70+ Female Married 2018 1 Sunday
Respiratory vaccines 0-19 Male Single/widowed 2016 2 Friday
Respiratory vaccines 60-69 Male Divorced 2017 1 Friday
Respiratory vaccines 60-69 Male Single/widowed 2016 1 Saturday
Respiratory vaccines 70+ Female NA 2016 1 Tuesday
Respiratory vaccines 60-69 Female Single/widowed 2017 3 Thursday
Respiratory vaccines 70+ Male Married 2022 4 Wednesday
Respiratory vaccines 30-39 Male Married 2023 1 Saturday
Respiratory vaccines 60-69 Female Single/widowed 2015 4 Wednesday
Respiratory vaccines 50-59 Female Married 2015 4 Monday
Respiratory vaccines 0-19 Female NA 2021 2 Friday
Respiratory vaccines 60-69 Female Married 2015 4 Wednesday
Respiratory vaccines 0-19 Male Single/widowed 2019 3 Wednesday
Respiratory vaccines 20-29 Male Single/widowed 2022 3 Sunday
Respiratory vaccines 70+ Male NA 2024 4 Friday
Respiratory vaccines 20-29 Female Divorced 2022 3 Thursday
Respiratory vaccines 60-69 Male Married 2024 2 Friday
Respiratory vaccines 60-69 Male Married 2015 2 Friday
Respiratory vaccines 60-69 Female Single/widowed 2024 2 Thursday
Respiratory vaccines 60-69 Female Married 2021 3 Monday
Respiratory vaccines 70+ Male Divorced 2016 4 Wednesday
Respiratory vaccines 60-69 Male Married 2020 2 Thursday
Respiratory vaccines 40-49 Female Married 2023 3 Saturday
Respiratory vaccines 40-49 Female Married 2017 1 Saturday
Respiratory vaccines 70+ Male Married 2019 4 Saturday
Respiratory vaccines 20-29 Female Married 2015 2 Friday
Respiratory vaccines 20-29 Male Divorced 2015 4 Tuesday
Respiratory vaccines 70+ Male Married 2019 1 Wednesday
Respiratory vaccines 40-49 Male Divorced 2021 4 Friday
Respiratory vaccines 60-69 Male Married 2017 3 Friday
Respiratory vaccines 40-49 Female Divorced 2023 2 Saturday
Respiratory vaccines 20-29 Male Married 2015 2 Thursday
Respiratory vaccines 0-19 Female NA 2020 2 Wednesday
Respiratory vaccines 0-19 Male Single/widowed 2016 2 Friday
Respiratory vaccines 20-29 Male Single/widowed 2019 2 Monday
Respiratory vaccines 20-29 Male NA 2018 1 Friday
Respiratory vaccines 0-19 Male Single/widowed 2020 1 Friday
Respiratory vaccines 40-49 Female Married 2017 1 Wednesday
Respiratory vaccines 30-39 Male NA 2023 4 Monday
Respiratory vaccines 50-59 Female Married 2018 1 Saturday
Respiratory vaccines 60-69 Male Married 2016 1 Thursday
Respiratory vaccines 60-69 Female Single/widowed 2016 1 Friday
Respiratory vaccines 20-29 Male Divorced 2022 1 Friday
Respiratory vaccines 40-49 Male Married 2024 3 Saturday
Respiratory vaccines 20-29 Male Married 2015 4 Monday
Respiratory vaccines 70+ Male Married 2019 3 Wednesday
Respiratory vaccines 30-39 Male Divorced 2016 2 Saturday
Respiratory vaccines 20-29 Female Single/widowed 2015 2 Sunday
Respiratory vaccines 0-19 Female Married 2017 1 Thursday
Respiratory vaccines 40-49 Female Married 2022 1 Thursday
Respiratory vaccines 60-69 Male Divorced 2020 3 Friday
Respiratory vaccines 50-59 Male Married 2023 1 Wednesday
Respiratory vaccines 20-29 Female Married 2019 1 Saturday
Respiratory vaccines 0-19 Male Married 2018 2 Friday
Respiratory vaccines 0-19 Female Married 2021 3 Friday
Respiratory vaccines 50-59 Female Single/widowed 2018 3 Tuesday
Respiratory vaccines 30-39 Male Single/widowed 2021 4 Wednesday
Respiratory vaccines 60-69 Female Married 2021 3 Sunday
Respiratory vaccines 60-69 Male Married 2020 4 Thursday
Respiratory vaccines 40-49 Male Married 2018 2 Monday
Respiratory vaccines 20-29 Female Single/widowed 2015 3 Tuesday
create_tab <- function(data, var, label) {
  tab_main <- data |>
    filter(!is.na({{ var }})) |>
    tabyl({{ var }}, category) |>
    mutate(Category = label, .before = 1) |>
    rename(Label = {{ var }}) |>
    mutate(Label = as.character(Label))

  na_count <- sum(is.na(pull(data, {{ var }})))
  if (na_count > 0) {
    # Dynamically build missing row across all category levels
    missing_row <- data |>
      filter(is.na({{ var }})) |>
      count(category) |>
      pivot_wider(names_from = category, values_from = n, values_fill = 0) |>
      mutate(Category = label, Label = "Missing", .before = 1)

    tab_final <- bind_rows(tab_main, missing_row)
  } else {
    tab_final <- tab_main
  }

  return(tab_final)
}
tab_age <- create_tab(vacc_1524, age_group, "Age")
tab_sex <- create_tab(vacc_1524, gender, "Gender")
tab_marital_status <- create_tab(vacc_1524, marital_status, "Marital status")
tab_year <- create_tab(vacc_1524, vacc_service_year, "Year")
tab_quarter <- create_tab(vacc_1524, vacc_quarter, "Quarter")
tab_weekday <- create_tab(vacc_1524, vacc_weekday, "Weekday")
tab_combined <- bind_rows(
  tab_sex,
  tab_age,
  tab_marital_status,
  tab_year,
  tab_quarter,
  tab_weekday
)
tab_combined
       Category          Label Respiratory vaccines Childhood vaccines Other
         Gender         Female                  445                232    65
         Gender           Male                  510                240    61
            Age           0-19                  146                 66    23
            Age          20-29                  160                 81    17
            Age          30-39                  131                 76    18
            Age          40-49                  134                 58    17
            Age          50-59                  126                 65    19
            Age          60-69                  135                 65    14
            Age            70+                  123                 61    18
 Marital status       Divorced                  117                 52    15
 Marital status        Married                  458                218    61
 Marital status Single/widowed                  283                156    41
 Marital status        Missing                   97                 46     9
           Year           2015                  110                 46     8
           Year           2016                   99                 45    14
           Year           2017                   81                 45    10
           Year           2018                   83                 47    13
           Year           2019                  105                 51    12
           Year           2020                   98                 45    12
           Year           2021                   87                 52    12
           Year           2022                   90                 40     9
           Year           2023                   96                 46    17
           Year           2024                  106                 55    19
        Quarter              1                  237                115    40
        Quarter              2                  237                122    36
        Quarter              3                  228                117    22
        Quarter              4                  253                118    28
        Weekday         Friday                  152                 64    16
        Weekday         Monday                  127                 59    20
        Weekday       Saturday                  111                 68    22
        Weekday         Sunday                  129                 73    12
        Weekday       Thursday                  145                 62    19
        Weekday        Tuesday                  138                 71    19
        Weekday      Wednesday                  153                 75    18
tab_combined <- tab_combined |>
  rowwise() |>
  mutate(
    Total = sum(
      `Respiratory vaccines`,
      `Childhood vaccines`,
      Other,
      na.rm = TRUE
    ),
    is_missing = trimws(tolower(Label)) == "missing",

    Respiratory = ifelse(
      is_missing,
      comma(`Respiratory vaccines`),
      paste0(
        comma(`Respiratory vaccines`),
        " (",
        round(100 * `Respiratory vaccines` / Total, 1),
        ")"
      )
    ),

    Childhood = ifelse(
      is_missing,
      comma(`Childhood vaccines`),
      paste0(
        comma(`Childhood vaccines`),
        " (",
        round(100 * `Childhood vaccines` / Total, 1),
        ")"
      )
    ),

    Other_grp = ifelse(
      is_missing,
      comma(Other),
      paste0(comma(Other), " (", round(100 * Other / Total, 1), ")")
    )
  ) |>
  ungroup() |>
  select(Category, Label, Respiratory, Childhood, Other_grp)

tab_combined
# A tibble: 34 × 5
   Category       Label    Respiratory Childhood  Other_grp
   <chr>          <chr>    <chr>       <chr>      <chr>    
 1 Gender         Female   445 (60)    232 (31.3) 65 (8.8) 
 2 Gender         Male     510 (62.9)  240 (29.6) 61 (7.5) 
 3 Age            0-19     146 (62.1)  66 (28.1)  23 (9.8) 
 4 Age            20-29    160 (62)    81 (31.4)  17 (6.6) 
 5 Age            30-39    131 (58.2)  76 (33.8)  18 (8)   
 6 Age            40-49    134 (64.1)  58 (27.8)  17 (8.1) 
 7 Age            50-59    126 (60)    65 (31)    19 (9)   
 8 Age            60-69    135 (63.1)  65 (30.4)  14 (6.5) 
 9 Age            70+      123 (60.9)  61 (30.2)  18 (8.9) 
10 Marital status Divorced 117 (63.6)  52 (28.3)  15 (8.2) 
# ℹ 24 more rows
tab_combined <- tab_combined |>
  group_by(Category) |>
  mutate(Category = ifelse(row_number() == 1, Category, "")) |>
  ungroup()

tab_combined
# A tibble: 34 × 5
   Category         Label    Respiratory Childhood  Other_grp
   <chr>            <chr>    <chr>       <chr>      <chr>    
 1 "Gender"         Female   445 (60)    232 (31.3) 65 (8.8) 
 2 ""               Male     510 (62.9)  240 (29.6) 61 (7.5) 
 3 "Age"            0-19     146 (62.1)  66 (28.1)  23 (9.8) 
 4 ""               20-29    160 (62)    81 (31.4)  17 (6.6) 
 5 ""               30-39    131 (58.2)  76 (33.8)  18 (8)   
 6 ""               40-49    134 (64.1)  58 (27.8)  17 (8.1) 
 7 ""               50-59    126 (60)    65 (31)    19 (9)   
 8 ""               60-69    135 (63.1)  65 (30.4)  14 (6.5) 
 9 ""               70+      123 (60.9)  61 (30.2)  18 (8.9) 
10 "Marital status" Divorced 117 (63.6)  52 (28.3)  15 (8.2) 
# ℹ 24 more rows
totals <- vacc_1524 |> count(category) |> deframe()
rm(
  tab_age,
  tab_sex,
  tab_marital_status,
  tab_year,
  tab_quarter,
  tab_weekday,
  tab_combined,
  #ft_vacc_summary,
  totals,
  create_tab
)
# ------------1. Table without title and footer -------------------------------------------+
library(dplyr)
library(janitor)
library(tibble)
library(scales)
library(flextable)
#.......................Ensure grouping variables are characters............................
vacc_1524 <- vacc_1524 |>
  mutate(across(c(age_group, gender, marital_status, vacc_service_year, vacc_quarter, vacc_weekday), as.character))

# ---------------- Table generation function ....................
create_tab <- function(data, var, label) {
  tab_main <- data |>
    filter(!is.na({{ var }})) |>
    tabyl({{ var }}, category) |>
    mutate(Category = label, .before = 1) |>
    rename(Label = {{ var }}) |>
    mutate(Label = as.character(Label))
  na_count <- sum(is.na(pull(data, {{ var }})))
  
  if (na_count > 0) {
    missing_row <- tibble(
      Category = label,
      Label = "Missing",
      `Respiratory vaccines` = sum(data$vacc_group3 == "Respiratory vaccines" & is.na(pull(data, {{ var }})), na.rm = TRUE),
      `Childhood vaccines` = sum(data$vacc_group3 == "Childhood vaccines" & is.na(pull(data, {{ var }})), na.rm = TRUE),
      Other = sum(data$vacc_group3 == "Other" & is.na(pull(data, {{ var }})), na.rm = TRUE)
    )
    tab_final <- bind_rows(tab_main, missing_row)
  } else {
    tab_final <- tab_main
  }
  
  return(tab_final)  
}

# .................... Create summary tables ....................
tab_age <- create_tab(vacc_1524, age_group, "Age")
tab_sex <- create_tab(vacc_1524, gender, "Gender")
tab_marital_status <- create_tab(vacc_1524, marital_status, "Marital status")
tab_year <- create_tab(vacc_1524, vacc_service_year, "Year")
tab_quarter <- create_tab(vacc_1524, vacc_quarter, "Quarter")
tab_weekday <- create_tab(vacc_1524, vacc_weekday, "Weekday")

# ......................Combine all tabs ....................
tab_combined <- bind_rows(
  tab_sex,
  tab_age,
  tab_marital_status,
  tab_year,
  tab_quarter,
  tab_weekday
)

# ....................... Create row % and format as text ....................
tab_combined <- tab_combined |>
  rowwise() |>
  mutate(
    Total = sum(
      `Respiratory vaccines`,
      `Childhood vaccines`,
      Other,
      na.rm = TRUE
    ),
    is_missing = trimws(tolower(Label)) == "missing",
    
    Respiratory = ifelse(is_missing,
                         comma(`Respiratory vaccines`),
                         paste0(comma(`Respiratory vaccines`), " (", round(100 * `Respiratory vaccines` / Total, 1), ")")),
    
    Childhood = ifelse(is_missing,
                       comma(`Childhood vaccines`),
                       paste0(comma(`Childhood vaccines`), " (", round(100 * `Childhood vaccines` / Total, 1), ")")),
    
    Other_grp = ifelse(is_missing,
                       comma(Other),
                       paste0(comma(Other), " (", round(100 * Other / Total, 1), ")"))  
  ) |>                                                                                   
  ungroup() |>
  select(Category, Label, Respiratory, Childhood, Other_grp)

# ............................. Remove repeated variable names ....................
tab_combined <- tab_combined |>
  group_by(Category) |>
  mutate(Category = ifelse(row_number() == 1, Category, "")) |>
  ungroup()

# ........................... Get total counts per vaccine group ....................
totals <- vacc_1524 |>
  count(category) |>
  deframe()

# .................... Create flextable ....................
ft_vacc_summary <- tab_combined |>
  flextable() |>
  set_header_labels(
    Category = "Variable type",
    Label = "Values",
    Respiratory = "Number (%)ᵃ",
    Childhood = "Number (%)ᵃ",
    Other_grp = "Number (%)ᵃ"
  ) |>
  add_header_row(
    # Second row
    values = c(
      "",
      "",
      paste0(
        "Respiratory vaccines (N = ",
        format(totals["Respiratory vaccines"], big.mark = ","),
        ")"
      ),
      paste0(
        "Childhood vaccines (N = ",
        format(totals["Childhood vaccines"], big.mark = ","),
        ")"
      ),
      paste0("Other (N = ", format(totals["Other"], big.mark = ","), ")")
    )
  ) |>
  add_header_row(
    # First row (merged header)
    values = c("Description", "Vaccination uptake"),
    colwidths = c(2, 3)
  ) |>
  theme_box() |>
  set_table_properties(layout = "autofit", width = 1) |>
  width(j = 1, width = 1.5) |>
  width(j = 2, width = 2.5) |>
  width(j = 3:4, width = 1.5) |>
  align(align = "justify", part = "all") |>
  border_remove() |>
  border(
    part = "header",
    i = 1,
    border.top = fp_border(color = "black", width = 1)
  ) |>
  border(
    part = "header",
    i = 3,
    border.bottom = fp_border(color = "black", width = 1)
  ) |>
  border_inner_h(
    part = "header",
    border = fp_border(color = "black", width = 0.8)
  ) |>
#  border_inner_h(
#    part = "body",
#    border = fp_border(color = "black", width = 0.8)
#  ) |>
#  border(
#    part = "body",
#    i = nrow(tab_combined),
#    border.bottom = fp_border(color = "black", width = 1)
#  ) |>
  set_caption(as_paragraph(as_chunk("Table: Vaccination uptake across service year, quarter, and weekday",
                                  props = fp_text(font.size = 14, font.family = "Times New Roman", bold = FALSE)))) |>
  fontsize(part = "header", size = 12) |>
  fontsize(part = "body", size = 11) |>
  fontsize(part = "footer", size = 10) |>
  font(part = "all", fontname = "Times New Roman") |>
  bg(part = "header", bg = "lightblue") |>
  bg(j = 1:5, i = seq(1, nrow(tab_combined), 2), bg = "white") |>
  bg(j = 1:5, i = seq(2, nrow(tab_combined), 2), bg = "lightgray") |>
  add_footer_lines(
    values = "ᵃ Vaccination proportions were calculated row-wise (i.e., by row)."
  )

# --- Display the table ---
ft_vacc_summary

Description

Vaccination uptake

Respiratory vaccines (N = 955)

Childhood vaccines (N = 472)

Other (N = 126)

Variable type

Values

Number (%)ᵃ

Number (%)ᵃ

Number (%)ᵃ

Gender

Female

445 (60)

232 (31.3)

65 (8.8)

Male

510 (62.9)

240 (29.6)

61 (7.5)

Age

0-19

146 (62.1)

66 (28.1)

23 (9.8)

20-29

160 (62)

81 (31.4)

17 (6.6)

30-39

131 (58.2)

76 (33.8)

18 (8)

40-49

134 (64.1)

58 (27.8)

17 (8.1)

50-59

126 (60)

65 (31)

19 (9)

60-69

135 (63.1)

65 (30.4)

14 (6.5)

70+

123 (60.9)

61 (30.2)

18 (8.9)

Marital status

Divorced

117 (63.6)

52 (28.3)

15 (8.2)

Married

458 (62.1)

218 (29.6)

61 (8.3)

Single/widowed

283 (59)

156 (32.5)

41 (8.5)

Missing

0

0

0

Year

2015

110 (67.1)

46 (28)

8 (4.9)

2016

99 (62.7)

45 (28.5)

14 (8.9)

2017

81 (59.6)

45 (33.1)

10 (7.4)

2018

83 (58)

47 (32.9)

13 (9.1)

2019

105 (62.5)

51 (30.4)

12 (7.1)

2020

98 (63.2)

45 (29)

12 (7.7)

2021

87 (57.6)

52 (34.4)

12 (7.9)

2022

90 (64.7)

40 (28.8)

9 (6.5)

2023

96 (60.4)

46 (28.9)

17 (10.7)

2024

106 (58.9)

55 (30.6)

19 (10.6)

Quarter

1

237 (60.5)

115 (29.3)

40 (10.2)

2

237 (60)

122 (30.9)

36 (9.1)

3

228 (62.1)

117 (31.9)

22 (6)

4

253 (63.4)

118 (29.6)

28 (7)

Weekday

Friday

152 (65.5)

64 (27.6)

16 (6.9)

Monday

127 (61.7)

59 (28.6)

20 (9.7)

Saturday

111 (55.2)

68 (33.8)

22 (10.9)

Sunday

129 (60.3)

73 (34.1)

12 (5.6)

Thursday

145 (64.2)

62 (27.4)

19 (8.4)

Tuesday

138 (60.5)

71 (31.1)

19 (8.3)

Wednesday

153 (62.2)

75 (30.5)

18 (7.3)

ᵃ Vaccination proportions were calculated row-wise (i.e., by row).

datatable(
  tab_combined,
  rownames = FALSE,
  extensions = 'Buttons',
  options = list(
    dom = 'Bfrtip',
    buttons = c('copy', 'csv', 'excel', 'pdf', 'print'),
    pageLength = 25,
    autoWidth = TRUE,
    columnDefs = list(list(width = '200px', targets = c(0, 1)))
  ),
  caption = htmltools::tags$caption(
    style = 'caption-side: top; font-size: 18px; font-family: "Times New Roman"; font-weight: bold;',
    'Table: Vaccination uptake across service year, quarter, and weekday'
  ),
  class = 'stripe hover cell-border'
)