Skip to contents

ltsummary tables are rendered by the lt package, which builds the HTML table in the browser from a small JSON specification. That makes HTML output the native format, and it is the format this website uses. Other formats are reached through lt’s static rendering or by exporting the table to a file. This article says what works where, and how.

Output types

Format How Result
HTML (Quarto, R Markdown, litedown) print the table full output
GitHub Flavored Markdown print the table with the static option set full output
Word, PDF and typst (Quarto) print the table with the static option set native table; indentation and bold cells are lost
Word and PDF (R Markdown) export to PNG and include the image image of the full table
PNG, PDF and HTML files lt::lt_export() full output, one page sized to the table
Paginated PDF file lt::lt_export() with crop = FALSE full output on letter-size pages, header and footnotes repeated

HTML

Printing a table in a chunk is enough. The package registers methods for knitr::knit_print() and for litedown’s xfun::record_print(), so the table renders in Quarto, R Markdown and litedown documents without any extra code.

```{r}
library(ltsummary)
tbl_summary(trial, by = trt, include = c(age, grade)) |> add_p()
```

The JavaScript and CSS that lt needs are emitted once per document, with the first table.

Word, PDF and typst with Quarto

lt can bake a static <table> instead of the JavaScript specification. Set the lt.lt_static option at the top of the document and print tables as usual; Quarto then converts the HTML table into a native Word, LaTeX or typst table.

```{r}
#| include: false
options(lt.lt_static = list(fragment = TRUE, css = FALSE))
library(ltsummary)
```

```{r}
tbl_summary(trial, by = trt, include = c(age, grade)) |> add_p() |> bold_labels()
```

Baking the table needs Node.js or a Chromium-based browser on the machine that renders the document; lt finds either on its own. The conversion keeps the headers, the values and the footnotes, and drops the inline styles, so the indentation of the levels and bold or italic cells do not survive. This is the same limitation that gtsummary documents for its kable output.

The option is what makes the table appear at all in these formats. Without it the chunk emits lt’s JavaScript specification, Pandoc drops that from Word, PDF and typst output, and the table is missing from the document with no warning from Quarto or knitr. A format: typst document renders with the same option and needs no LaTeX installation.

The same option makes the table render in GitHub Flavored Markdown, for example in a README built from an R Markdown file, because GitHub displays raw HTML tables.

Word and PDF with R Markdown

Pandoc drops raw HTML when it writes Word or LaTeX, so in R Markdown the static option is not enough. Export the table to an image and include it:

```{r, echo = FALSE}
tbl <- tbl_summary(trial, by = trt, include = c(age, grade)) |> add_p()
lt::lt_export(as_lt(tbl), "table1.png")
knitr::include_graphics("table1.png")
```

The image is cropped to the table and keeps every formatting feature. lt_export() renders through a headless Chromium browser; cropping PNG output also needs the magick package.

Alternatively, switch to Quarto for the document: the Quarto route above gives a real table that Word users can edit.

Images and files

lt::lt_export() writes a table to disk. The file extension chooses the format: .html for a standalone web page, .pdf for a vector PDF, and anything else for a PNG. PDF and PNG are rendered by a headless Chromium-based browser, which lt finds on its own.

tbl <- tbl_summary(trial, by = trt, include = c(age, grade)) |> add_p()

lt::lt_export(as_lt(tbl), "table1.html")
lt::lt_export(as_lt(tbl), "table1.pdf")
lt::lt_export(as_lt(tbl), "table1.png", width = 500)

A PDF comes in two shapes. The default, crop = TRUE, is a single page sized to the table, for placing the table into another document as a figure. lt 0.4 measures that page a fraction of a pixel too short when the table has two rows of footnotes, as a summary table with add_p() does, and the browser then adds a second page holding a repeated header and the footnotes. lt’s development version fixes this (yihui/lt#6, for lt 0.5); with lt 0.4 from CRAN, check the page count of a cropped export.

With crop = FALSE the table is printed on letter-size pages, and the column header and the footnotes repeat on every page. With xfun 0.60 from CRAN the browser also prints its own header and footer (the date, the page title and the path of a temporary file); the flag below turns that off, and xfun’s development version passes it by default (yihui/xfun#130, for xfun 0.61). Keep "default" in args: it stands for the headless flags, and without it the browser opens the file in a window and writes no PDF.

lt::lt_export(as_lt(tbl), "table1.pdf", crop = FALSE,
              args = c("default", "--no-pdf-header-footer"))

The exported page uses the system font. A stylesheet changes it, and for crop = FALSE an @page rule in the same stylesheet sets the page size and margins. With lt 0.4, give as_lt() an absolute path: a relative one is written into the page as a link that does not resolve from the temporary directory the browser renders in. lt’s development version resolves relative paths at export time (yihui/lt#8).

# submission.css:
# @page { size: letter; margin: 0.375in 0.375in 0.375in 0.75in; }
# body { font-family: "Times New Roman", Times, serif; }
# td, th { font-size: 10pt; }
lt::lt_export(as_lt(tbl, css = normalizePath("submission.css")), "table1.pdf",
              crop = FALSE, args = c("default", "--no-pdf-header-footer"))

The README of the package is built this way: the table is exported to PNG and the image is included, which is why it shows on GitHub.

Plain text

as.data.frame() returns the formatted cells as a data frame, with the header labels as column names. Print it, or pass it to knitr::kable(), when a plain table is all that is needed. The data.frame print engine does the same at the console.

tbl <- tbl_summary(trial, by = trt, include = c(age, grade)) |> add_p()
as.data.frame(tbl)
#>   **Characteristic** **Drug A**  \nN = 95 **Drug B**  \nN = 105 **p-value**
#> 1                Age          59 (51, 66)           62 (54, 71)       0.043
#> 2            Unknown                    4                     7        <NA>
#> 3              Grade                 <NA>                  <NA>         0.3
#> 4                  I             25 (26%)              37 (35%)        <NA>
#> 5                 II             40 (42%)              43 (41%)        <NA>
#> 6                III             30 (32%)              25 (24%)        <NA>

The header labels carry markdown (**Drug A**) and line breaks, which kable() does not interpret, and empty cells are NA. Tidy both before passing the data frame on:

df <- as.data.frame(tbl)
names(df) <- gsub("  \n", ", ", gsub("**", "", names(df), fixed = TRUE))
df[is.na(df)] <- ""
knitr::kable(df)
Characteristic Drug A, N = 95 Drug B, N = 105 p-value
Age 59 (51, 66) 62 (54, 71) 0.043
Unknown 4 7
Grade 0.3
I 25 (26%) 37 (35%)
II 40 (42%) 43 (41%)
III 30 (32%) 25 (24%)

Tips

When printing tables in a loop, use print() in a chunk with results = "asis":

```{r loop_print, results = "asis"}
for (v in c("age", "grade")) {
  tbl <- tbl_summary(trial, by = trt, include = all_of(v))
  print(tbl)
}
```

If print(tbl) does not render, try knitr::knit_print(tbl) or cat(knitr::knit_print(tbl)).

inline_text() quotes the table in the text; the inline_text() tutorial shows how.