R Markdown issue: output table blurry

Hello all,

I’m looking for assistance because the tables I’m attempting to output in R markdown come out blurry. I’m creating the table using gt, saving the table, the knitting to a formatted Word document. After consulting multiple sources, including stack overflow, I’ve tried adjusting table dimensions and dpi (dpi=300), but I still have a blurry output.

I’m hoping to get this sorted out in the next week or so. Appreciate any help - thanks in advance!

Michelle


title: “Reprex_table_recreate”

author: “Michelle Azar”
date: “r Sys.Date()”
output: word_document

knitr::opts_chunk$set(
	echo = TRUE,
	error = TRUE,
	message = FALSE,
	warning = FALSE
)
#Load packages
pacman::p_load(
     officedown,   # for r markdown
     webshot2,     # for saving gt tables as png
     rio,          # for importing data
     here,         # for locating files
     skimr,        # for reviewing the data
     janitor,      # for data cleaning  
     datapasta,    # for creating reprex
     epikit,       # creating age categories
     dpasta,       # for creating demo data
     gtsummary,    # creating tables  
     RColorBrewer, # for colour palettes
     viridis,      # for more colour palettes
     scales,       # percents in tables 
     zoo,          # manipulate dates
     gt,           # make better tables
     flextable,    # for making pretty tables
     gghighlight,  # highlighting plot parts  
     ggExtra,      # special plotting functions
     apyramid,     #for age/sex pyramids
     viridis,      #for color-blind friendly scales
     tsibble,      #for epiweeks and other time series analyses
     tidyverse     # for data management and visualization
)
table1_processed<-data.frame(
  stringsAsFactors = FALSE,
                        disease = c("Campylobacteriosis","Cryptosporidiosis",
                                    "Cyclosporiasis","Dengue fever","Ehrlichiosis",
                                    "Giardiasis",
                                    "Haemophilus influenzae, invasive","Hepatitis A, acute",
                                    "Hepatitis B, acute","Hepatitis C, acute",
                                    "Hepatitis C, chronic",
                                    "Invasive pneumococcal disease (IPD)","Latent TB Infection (LTBI)",
                                    "Legionellosis","Listeriosis","Malaria",
                                    "Meningococcal disease","Mpox","Mumps",
                                    "Nontuberculous Mycobacteria (NTM)","Pertussis",
                                    "STEC (Shiga Toxin-producing E. coli)",
                                    "Salmonellosis","Shiga toxin positive (Unk. organism)",
                                    "Shigellosis",
                                    "Spotted Fever Rickettsiosis (SFR, including RMSF)",
                                    "Staph aureus, VISA","TB Disease",
                                    "Toxic shock syndrome (strep or staph)","Vibriosis","West Nile Virus",
                                    "Yersiniosis","Zika"),
                           type = c("Enteric","Enteric","Enteric","Vector Borne",
                                    "Vector Borne","Enteric","Systemic",
                                    "Enteric","Systemic","Systemic","Systemic",
                                    "Systemic","Mycobacterial","Systemic",
                                    "Systemic","Vector Borne","Systemic",
                                    "Systemic","Systemic","Mycobacterial",
                                    "Vaccine-Preventable","Enteric","Enteric","Enteric",
                                    "Enteric","Vector Borne","Systemic",
                                    "Mycobacterial","Systemic","Enteric",
                                    "Vector Borne","Enteric","Vector Borne"),
                case_count_2026 = c(103L,
                                    21L,236L,1L,128L,29L,8L,4L,13L,1L,
                                    384L,0L,134L,17L,1L,1L,0L,2L,0L,
                                    109L,35L,32L,113L,0L,26L,3L,6L,11L,6L,
                                    3L,0L,17L,0L),
                prev_fiveyr_med = c(74L,
                                    16L,16L,0L,128L,17L,16L,1L,3L,2L,
                                    191L,0L,257L,20L,1L,2L,0L,2L,0L,86L,
                                    3L,23L,86L,0L,12L,3L,6L,11L,2L,3L,
                                    4L,12L,0L),
                    perc_change = c("39%",
                                    "31%","1375%",NA,"0%","71%","-50%",
                                    "300%","333%","-50%","101%",NA,"-48%",
                                    "-15%","0%","-50%",NA,"0%",NA,"27%",
                                    "1067%","39%","31%",NA,"117%","0%","0%",
                                    "0%","200%","0%","-100%","42%",NA)
             )
overview_table<-table1_processed %>% 
  gt(rowname_col = "disease", groupname_col = "type") %>% 
  opt_table_font(color="black",font="Lexend Light",weight="normal",size = 13) %>% 
  summary_rows(
    groups = everything(),
    columns = case_count_2026,
    fns = list(Total=~sum(.,na.rm = TRUE))
  ) %>% 
  tab_options(summary_row.background.color = "lightgray") %>% 
  cols_align(align="center",
             columns = c("case_count_2026", "prev_fiveyr_med", "perc_change")) %>%
  cols_width("case_count_2026" ~ px(100),
             "prev_fiveyr_med" ~px(150),
             "perc_change" ~ px(100)) %>% 
   cols_label(
    case_count_2026 = md("**Year-to-Date Cases**"),
    prev_fiveyr_med = md("**Previous 5-Year Median: Year-to-Date**"),
    perc_change = md("**Percent Change**")
  )  

gtsave(overview_table, filename = "overview_table.png")
knitr::include_graphics("overview_table.png")

Hey Michelle!

The blurriness isn’t coming from your knitr chunk settings. dpi=300 in knitr::opts_chunk$set() only controls the resolution of plots that R renders live inside a chunk, like a ggplot. Your table isn’t one of those: gtsave() already rendered overview_table.png as a static image via webshot2 before knitr ever sees it, and knitr::include_graphics() just drops that existing file in. It can’t sharpen a low-res PNG after the fact.

The resolution of that PNG is controlled by webshot2’s zoom argument, which gtsave() passes through. By default it renders at zoom = 1, which looks fine on screen but comes out fuzzy once it’s placed in a Word doc. Try this:

gtsave(
  overview_table,
  filename = "overview_table.png",
  zoom = 4,        # renders at 4x pixel density, same layout
  expand = 10      # optional: small margin so nothing gets clipped
)

zoom = 3 or 4 is usually enough to look crisp in a printed/Word context; go higher only if you still see softness.

If you want the table to be a native, editable Word table instead of an embedded image, so it doesn’t rely on rasterization at all, that’s a different route. flextable ( 29  Tables for presentation – The Epidemiologist R Handbook ) , which you already have loaded, renders directly into word_document output without needing webshot2/PNG.

Hi Luis,

Thanks so much for your thorough and quick reply; it was very helpful!

That seemed to help quite a bit and is good to learn for the future.

I looked into using flextable as well, but I couldn’t find documentation in the Epidemiologist R Handbook (or elsewhere), if that had the ability to create subtotal rows by disease type (enteric, vector-borne, systemic, etc.). Is that possible in flextable?

Apologies if I’ve missed that somewhere, and thanks again!

Michelle

Glad that helped!

Short answer: yes, but flextable doesn’t have a built-in equivalent to gt’s summary_rows() that computes subtotals for you.

You need to add the subtotal rows to your data yourself. Havent used this myself but you can try as_grouped_data() to handle the group display.

Example:

library(dplyr)
library(flextable)

table1_with_subtotals <- table1_processed |>
  group_by(type) |>
  group_modify(~ bind_rows(
    .x,
    summarise(.x, disease = "Subtotal", case_count_2026 = sum(case_count_2026, na.rm = TRUE))
  )) |>
  ungroup()

ft <- table1_with_subtotals |>
  as_grouped_data(groups = "type") |>
  as_flextable() |>
  bold(i = ~ disease == "Subtotal") |>
  bg(i = ~ disease == "Subtotal", bg = "lightgray")

as_grouped_data() is what turns your type column into gt style group header rows, similar to what groupname_col did for you in gt(). But it only handles the grouping and display; it doesn’t calculate anything. The group_modify() step above is doing the actual work gt’s summary_rows() did for you automatically.

Let me know if this helps further!

Luis