Tiny Tables

tinytable is a small but powerful R package to draw HTML, LaTeX, Word, PDF, Markdown, and Typst tables. The interface is minimalist, but it gives users direct and convenient access to powerful frameworks to create endlessly customizable tables.

Install the latest version from R-Universe or CRAN:

install.packages("tinytable",
  repos = c("https://vincentarelbundock.r-universe.dev", "https://cran.r-project.org")
)

This tutorial introduces the main functions of the package. It is also available as a single PDF document.

Load the library and set some global options:

library(tinytable)
options(tinytable_tt_digits = 3)
options(tinytable_latex_placement = "H")

Draw a first table:

x <- mtcars[1:4, 1:5]
tt(x)
mpg cyl disp hp drat
21 6 160 110 3.9
21 6 160 110 3.9
22.8 4 108 93 3.85
21.4 6 258 110 3.08

Width and height

The width arguments indicating what proportion of the line width the table should cover. This argument accepts a number between 0 and 1 to control the whole table width, or a vector of numeric values between 0 and 1, representing each column.

tt(x, width = 0.5)
mpg cyl disp hp drat
21 6 160 110 3.9
21 6 160 110 3.9
22.8 4 108 93 3.85
21.4 6 258 110 3.08
tt(x, width = 1)
mpg cyl disp hp drat
21 6 160 110 3.9
21 6 160 110 3.9
22.8 4 108 93 3.85
21.4 6 258 110 3.08

We can control individual columns by supplying a vector. In that case, the sum of width elements determines the full table width. For example, this table takes 70% of available width, with the first column 3 times as large as the other ones.

tt(x, width = c(.3, .1, .1, .1, .1))
mpg cyl disp hp drat
21 6 160 110 3.9
21 6 160 110 3.9
22.8 4 108 93 3.85
21.4 6 258 110 3.08

When the sum of the width vector exceeds 1, it is automatically normalized to full-width. This is convenient when we only want to specify column width in relative terms:

tt(x, width = c(3, 2, 1, 1, 1))
mpg cyl disp hp drat
21 6 160 110 3.9
21 6 160 110 3.9
22.8 4 108 93 3.85
21.4 6 258 110 3.08

When specifying a table width, the text is automatically wrapped to appropriate size:

lorem <- data.frame(
  Lorem = "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.",
  Ipsum = " Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos."
)

tt(lorem, width = 3 / 4)
Lorem Ipsum
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos.

The height argument controls the height of each row in em units:

tt(mtcars[1:4, 1:5], height = 3)
mpg cyl disp hp drat
21 6 160 110 3.9
21 6 160 110 3.9
22.8 4 108 93 3.85
21.4 6 258 110 3.08

Footnotes

The notes argument accepts single strings or named lists of strings:

n <- "Fusce id ipsum consequat ante pellentesque iaculis eu a ipsum. Mauris id ex in nulla consectetur aliquam. In nec tempus diam. Aliquam arcu nibh, dapibus id ex vestibulum, feugiat consequat erat. Morbi feugiat dapibus malesuada. Quisque vel ullamcorper felis. Aenean a sem at nisi tempor pretium sit amet quis lacus."

tt(lorem, notes = n, width = 1)

A full-width table with wrapped text in cells and a footnote.

Lorem Ipsum
Fusce id ipsum consequat ante pellentesque iaculis eu a ipsum. Mauris id ex in nulla consectetur aliquam. In nec tempus diam. Aliquam arcu nibh, dapibus id ex vestibulum, feugiat consequat erat. Morbi feugiat dapibus malesuada. Quisque vel ullamcorper felis. Aenean a sem at nisi tempor pretium sit amet quis lacus.
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos.

When notes is a named list, the names are used as identifiers and displayed as superscripts:

tt(x, notes = list(a = "Blah.", b = "Blah blah."))
mpg cyl disp hp drat
a Blah.
b Blah blah.
21 6 160 110 3.9
21 6 160 110 3.9
22.8 4 108 93 3.85
21.4 6 258 110 3.08

We can also add markers in individual cells by providing coordinates:

tt(x, notes = list(
  a = list(i = 0:1, j = 1, text = "Blah."),
  b = "Blah blah."
))
mpga cyl disp hp drat
a Blah.
b Blah blah.
21a 6 160 110 3.9
21 6 160 110 3.9
22.8 4 108 93 3.85
21.4 6 258 110 3.08

Output formats

tinytable can produce tables in HTML, Word, Markdown, LaTeX, Typst, PDF, or PNG format. An appropriate output format for printing is automatically selected based on (1) whether the function is called interactively, (2) is called within RStudio, and (3) the output format of the Rmarkdown or Quarto document, if applicable. Alternatively, users can specify the print format in print() or by setting a global option:

tt(x) |> print("markdown")
tt(x) |> print("html")
tt(x) |> print("latex")

options(tinytable_print_output = "markdown")

With the save_tt() function, users can also save tables directly to PNG (images), PDF or Word documents, and to any of the basic formats. All we need to do is supply a valid file name with the appropriate extension (ex: .png, .html, .pdf, etc.):

tt(x) |> save_tt("path/to/file.png")
tt(x) |> save_tt("path/to/file.pdf")
tt(x) |> save_tt("path/to/file.docx")
tt(x) |> save_tt("path/to/file.html")
tt(x) |> save_tt("path/to/file.tex")
tt(x) |> save_tt("path/to/file.md")

save_tt() can also return a string with the table in it, for further processing in R. In the first case, the table is printed to console with cat(). In the second case, it returns as a single string as an R object.

tt(mtcars[1:10, 1:5]) |>
  group_tt(
    i = list(
      "Hello" = 3,
      "World" = 8
    ),
    j = list(
      "Foo" = 2:3,
      "Bar" = 4:5
    )
  ) |>
  print("markdown")
+------+-----+------+-----+------+
|      | Foo        | Bar        |
+------+-----+------+-----+------+
| mpg  | cyl | disp | hp  | drat |
+======+=====+======+=====+======+
| 21   | 6   | 160  | 110 | 3.9  |
+------+-----+------+-----+------+
| 21   | 6   | 160  | 110 | 3.9  |
+------+-----+------+-----+------+
| Hello                          |
+------+-----+------+-----+------+
| 22.8 | 4   | 108  | 93  | 3.85 |
+------+-----+------+-----+------+
| 21.4 | 6   | 258  | 110 | 3.08 |
+------+-----+------+-----+------+
| 18.7 | 8   | 360  | 175 | 3.15 |
+------+-----+------+-----+------+
| 18.1 | 6   | 225  | 105 | 2.76 |
+------+-----+------+-----+------+
| 14.3 | 8   | 360  | 245 | 3.21 |
+------+-----+------+-----+------+
| World                          |
+------+-----+------+-----+------+
| 24.4 | 4   | 147  | 62  | 3.69 |
+------+-----+------+-----+------+
| 22.8 | 4   | 141  | 95  | 3.92 |
+------+-----+------+-----+------+
| 19.2 | 6   | 168  | 123 | 3.92 |
+------+-----+------+-----+------+ 
tt(mtcars[1:10, 1:5]) |>
  group_tt(
    i = list(
      "Hello" = 3,
      "World" = 8
    ),
    j = list(
      "Foo" = 2:3,
      "Bar" = 4:5
    )
  ) |>
  save_tt("markdown")
[1] "+------+-----+------+-----+------+\n|      | Foo        | Bar        |\n+------+-----+------+-----+------+\n| mpg  | cyl | disp | hp  | drat |\n+======+=====+======+=====+======+\n| 21   | 6   | 160  | 110 | 3.9  |\n+------+-----+------+-----+------+\n| 21   | 6   | 160  | 110 | 3.9  |\n+------+-----+------+-----+------+\n| Hello                          |\n+------+-----+------+-----+------+\n| 22.8 | 4   | 108  | 93  | 3.85 |\n+------+-----+------+-----+------+\n| 21.4 | 6   | 258  | 110 | 3.08 |\n+------+-----+------+-----+------+\n| 18.7 | 8   | 360  | 175 | 3.15 |\n+------+-----+------+-----+------+\n| 18.1 | 6   | 225  | 105 | 2.76 |\n+------+-----+------+-----+------+\n| 14.3 | 8   | 360  | 245 | 3.21 |\n+------+-----+------+-----+------+\n| World                          |\n+------+-----+------+-----+------+\n| 24.4 | 4   | 147  | 62  | 3.69 |\n+------+-----+------+-----+------+\n| 22.8 | 4   | 141  | 95  | 3.92 |\n+------+-----+------+-----+------+\n| 19.2 | 6   | 168  | 123 | 3.92 |\n+------+-----+------+-----+------+"

Combination and exploration

Tables can be explored, modified, and combined using many of the usual base R functions:

a <- tt(mtcars[1:2, 1:2])
a
mpg cyl
21 6
21 6
dim(a)
[1] 2 2
ncol(a)
[1] 2
nrow(a)
[1] 2
names(a)
[1] "mpg" "cyl"

Tables can be combined with the usual rbind() function:

a <- tt(mtcars[1:3, 1:2], caption = "Combine two tiny tables.")
b <- tt(mtcars[4:5, 8:10])

rbind(a, b)
Combine two tiny tables.
mpg cyl vs am gear
21 6 NA NA NA
21 6 NA NA NA
22.8 4 NA NA NA
NA NA vs am gear
NA NA 1 0 3
NA NA 0 0 3
rbind(a, b) |> format_tt(replace = "")
Combine two tiny tables.
mpg cyl vs am gear
21 6
21 6
22.8 4
vs am gear
1 0 3
0 0 3
Warning

format_tt() and style_tt() run only after rbind()/rbind2() finish combining the raw tables. If headers are inserted or column types differ, the joint data is coerced to character first, so rounding and styling operate on strings. Apply format_tt() to the raw data frame before calling tt(), or reapply formatting/styling after binding.

The rbind2() S4 method is slightly more flexible than rbind(), as it supports arguments headers and use_names.

Omit y header:

rbind2(a, b, headers = FALSE)
Combine two tiny tables.
mpg cyl vs am gear
21 6 NA NA NA
21 6 NA NA NA
22.8 4 NA NA NA
NA NA 1 0 3
NA NA 0 0 3

Bind tables by position rather than column names:

rbind2(a, b, use_names = FALSE)
Combine two tiny tables.
mpg cyl gear
21 6 NA
21 6 NA
22.8 4 NA
vs am gear
1 0 3
0 0 3

Select columns

The subset() function from base R can be used to select columns from a tinytable. This is especially useful when applying conditional styling based on column values, and then removing them. For example, if we have a table with 6 rows and three Species:

dat <- do.call(rbind, by(iris, ~Species, head, n = 2))
dat
              Sepal.Length Sepal.Width Petal.Length Petal.Width    Species
setosa.1               5.1         3.5          1.4         0.2     setosa
setosa.2               4.9         3.0          1.4         0.2     setosa
versicolor.51          7.0         3.2          4.7         1.4 versicolor
versicolor.52          6.4         3.2          4.5         1.5 versicolor
virginica.101          6.3         3.3          6.0         2.5  virginica
virginica.102          5.8         2.7          5.1         1.9  virginica

We highlight the versicolor rows in pink and remove the Species column:

tt(dat) |>
  style_tt(Species == "versicolor", background = "pink") |>
  subset(select = -Species)
Sepal.Length Sepal.Width Petal.Length Petal.Width
5.1 3.5 1.4 0.2
4.9 3 1.4 0.2
7 3.2 4.7 1.4
6.4 3.2 4.5 1.5
6.3 3.3 6 2.5
5.8 2.7 5.1 1.9

Or

tt(dat) |>
  style_tt(Species == "versicolor", background = "pink") |>
  subset(select = c(Sepal.Length, Sepal.Width))
Sepal.Length Sepal.Width
5.1 3.5
4.9 3
7 3.2
6.4 3.2
6.3 3.3
5.8 2.7

subset() can also be combined with group_tt() and style_tt(). In this example, we use Species for conditional styling and row grouping—both of which reference the original columns—and then call subset() last to drop the Species column:

tt(dat) |>
  style_tt(i = Species == "versicolor", color = "darkorange") |>
  group_tt(j = list("AAA" = 1:2, "BBB" = 3:4)) |>
  subset(select = -Sepal.Width)
AAA BBB
Sepal.Length Petal.Length Petal.Width Species
5.1 1.4 0.2 setosa
4.9 1.4 0.2 setosa
7 4.7 1.4 versicolor
6.4 4.5 1.5 versicolor
6.3 6 2.5 virginica
5.8 5.1 1.9 virginica
Important

Unlike most tinytable operations, subset() is applied eagerly—it modifies the table immediately rather than deferring work to build time. For more predictable results, call subset() as the last step in the pipeline. Calls to style_tt(), format_tt(), and group_tt() that appear before subset() will have their column indices automatically remapped to account for removed columns.

Rename columns

As noted above, tinytable tries to be standards-compliant, by defining methods for many base R functions. The benefit of this approach is that instead of having to learn a tinytable-specific syntax, users can rename columns using all the tools they already know:

a <- tt(mtcars[1:2, 1:2])
names(a) <- c("a", "b")
a
a b
21 6
21 6

In a pipe-based workflow, we can use the setNames() function from base R:

mtcars[1:2, 1:2] |>
  tt() |>
  setNames(c("a", "b"))
a b
21 6
21 6