
Using color_table() in R Markdown
Michael Friendly
2026-03-17
Source:vignettes/articles/color_table.Rmd
color_table.RmdOverview
color_table() produces a gt table object
with cell backgrounds shaded by observed frequencies or Pearson
residuals from an independence (or user-supplied) model. The goal is a
“smart” tabular display that makes patterns of association or unusual
cells immediately visible.
When used interactively in RStudio, the returned gt
object renders directly in the Viewer panel — no extra
steps needed. However, using color_table() in R Markdown
(.Rmd) or Quarto (.qmd) documents requires
some special consideration because gt tables render
natively only in HTML output. For PDF or Word
output, the table must be saved as a PNG image and included
with knitr::include_graphics().
The knit_include() helper in this package handles that
branching automatically. Piping a gt table through
knit_include() returns it unchanged for HTML output, and
saves a PNG fallback for all other formats — so the same chunk knits
correctly regardless of the output format.
HTML output — return the gt object directly
For documents that are compiled to HTML, color_table()
renders natively: just return the object from a chunk and
knitr handles it via gt’s built-in
knitr::knit_print() method. No filename
argument or extra wrapper is needed.
The first example shades cells by Pearson residuals from the independence model (the default), making over- and under-represented Hair×Eye combinations immediately visible.
data(HairEyeColor)
HEC <- margin.table(HairEyeColor, 1:2) # collapse over Sex
color_table(HEC, title = "Hair \u00d7 Eye Color (residual shading)")## Shading based on residuals from model of independence,
## X^2 = 138.29, df = 9, p = 2.325e-25
| Hair × Eye Color (residual shading) | |||||
|
Eye
|
Total | ||||
|---|---|---|---|---|---|
| Brown | Blue | Hazel | Green | ||
| Black | 68 | 20 | 15 | 5 | 108 |
| Brown | 119 | 84 | 54 | 29 | 286 |
| Red | 26 | 17 | 14 | 14 | 71 |
| Blond | 7 | 94 | 10 | 16 | 127 |
| Total | 220 | 215 | 93 | 64 | 592 |
For comparison, shading by raw frequencies (rather than residuals) gives a different picture — it highlights which combinations are simply most common, not which ones deviate from independence.
color_table(HEC, shade = "freq", title = "Hair \u00d7 Eye Color (frequency shading)")| Hair × Eye Color (frequency shading) | |||||
|
Eye
|
Total | ||||
|---|---|---|---|---|---|
| Brown | Blue | Hazel | Green | ||
| Black | 68 | 20 | 15 | 5 | 108 |
| Brown | 119 | 84 | 54 | 29 | 286 |
| Red | 26 | 17 | 14 | 14 | 71 |
| Blond | 7 | 94 | 10 | 16 | 127 |
| Total | 220 | 215 | 93 | 64 | 592 |
For a three-way table, a formula argument controls the
layout: variables on the left of ~ become row groups, those
on the right become column spanners. The legend note reproduces the
chi-squared summary printed to the console.
color_table(HairEyeColor,
formula = Eye ~ Hair + Sex,
legend = TRUE,
title = "Hair \u00d7 Eye \u00d7 Sex (complete independence residuals)")## Re-fitting to get frequencies and fitted values
## Shading based on residuals from model of complete independence, X^2 = 164.92, df = 24, p = 0
| Hair × Eye × Sex (complete independence residuals) | |||||
|
Eye
|
Total | ||||
|---|---|---|---|---|---|
| Brown | Blue | Hazel | Green | ||
| Black_Male | 32 | 11 | 10 | 3 | 56 |
| Black_Female | 36 | 9 | 5 | 2 | 52 |
| Brown_Male | 53 | 50 | 25 | 15 | 143 |
| Brown_Female | 66 | 34 | 29 | 14 | 143 |
| Red_Male | 10 | 10 | 7 | 7 | 34 |
| Red_Female | 16 | 7 | 7 | 7 | 37 |
| Blond_Male | 3 | 30 | 5 | 8 | 46 |
| Blond_Female | 4 | 64 | 5 | 8 | 81 |
| Total | 220 | 215 | 93 | 64 | 592 |
| Shading based on residuals from model of complete independence, X^2 = 164.92, df = 24, p = 0 | |||||
PDF / Word output — save image, then include it
For non-HTML output, save the table as a PNG and include with
knitr::include_graphics(). Supported formats:
.png, .pdf, .html,
.rtf, .docx. The vwidth and
vheight arguments control the image viewport in pixels.
The chunk below is shown for illustration only
(eval=FALSE); the HTML approach above is sufficient when
knitting to HTML.
color_table(HEC,
title = "Hair \u00d7 Eye Color",
filename = "color_table_hec.png",
vwidth = 520,
vheight = 300)
knitr::include_graphics("color_table_hec.png")Universal output — knit_include()
knit_include() eliminates the need to branch on output
format by hand. Pipe the gt object through it and the same
chunk works correctly whether you are knitting to HTML, PDF, or
Word.
color_table(HEC,
title = "Hair \u00d7 Eye Color") |>
knit_include(width = 520, height = 300)## Shading based on residuals from model of independence,
## X^2 = 138.29, df = 9, p = 2.325e-25
| Hair × Eye Color | |||||
|
Eye
|
Total | ||||
|---|---|---|---|---|---|
| Brown | Blue | Hazel | Green | ||
| Black | 68 | 20 | 15 | 5 | 108 |
| Brown | 119 | 84 | 54 | 29 | 286 |
| Red | 26 | 17 | 14 | 14 | 71 |
| Blond | 7 | 94 | 10 | 16 | 127 |
| Total | 220 | 215 | 93 | 64 | 592 |
knit_include() also handles htmlwidget
objects (plotly, DT, leaflet, …) the same way — HTML output gets the
interactive widget, PDF/Word output gets a screenshot. Any other class
is passed through unchanged, so it is safe to use in a pipe on any
object.
More examples
Displaying residual values in cells
Setting values = "residuals" replaces the cell
frequencies with the Pearson residual values themselves. This is useful
when the exact magnitude of each deviation matters, not just its
direction. Row and column totals are suppressed automatically because
residuals do not have meaningful marginal sums.
color_table(HEC,
values = "residuals",
title = "Hair \u00d7 Eye \u2014 Pearson residuals") |>
knit_include(width = 520, height = 280)## Shading based on residuals from model of independence,
## X^2 = 138.29, df = 9, p = 2.325e-25
| Hair × Eye — Pearson residuals | ||||
|
Eye
|
||||
|---|---|---|---|---|
| Brown | Blue | Hazel | Green | |
| Black | 4.40 | -3.07 | -0.48 | -1.95 |
| Brown | 1.23 | -1.95 | 1.35 | -0.35 |
| Red | -0.07 | -1.73 | 0.85 | 2.28 |
| Blond | -5.85 | 7.05 | -2.23 | 0.61 |
Multi-way table: PreSex data
The PreSex data cross-classifies four variables. Here
MaritalStatus is placed on rows and the two sexual-attitude
variables form nested column spanners, making the interaction structure
easy to read. The legend note records the goodness-of-fit for the
complete-independence model.
data(PreSex, package = "vcd")
color_table(PreSex,
formula = MaritalStatus ~ PremaritalSex + ExtramaritalSex,
legend = TRUE,
title = "Pre/Extra-marital Sex by Marital Status") |>
knit_include(width = 520, height = 300)## Re-fitting to get frequencies and fitted values
## Shading based on residuals from model of complete independence, X^2 = 270.14, df = 11, p = 0
| Pre/Extra-marital Sex by Marital Status | |||
|
MaritalStatus
|
Total | ||
|---|---|---|---|
| Divorced | Married | ||
| Yes_Yes | 45 | 15 | 60 |
| Yes_No | 114 | 67 | 181 |
| No_Yes | 53 | 8 | 61 |
| No_No | 282 | 452 | 734 |
| Total | 494 | 542 | 1036 |
| Shading based on residuals from model of complete independence, X^2 = 270.14, df = 11, p = 0 | |||