I found myself repeatedly writing similar code to generate pubplication ready plots that include LaTeX annotations for my papers and teaching materials. The tikzDevice
R package provides the foundation for combining R plots with LaTeX. I use the magick
library to convert the compiled PDF file to the desired output format. To streamline this workflow, I wrote a utility function that handles the entire pipeline.
create_latex_plot <- function(
plot_expr, # plot object / function call
out_name, # out files name
out_format = "png", # out format
out_dir = ".", # out directory
width = 9, # plot width
height = 6, # plot height
cleanup = T # remove intermediate files
) {
# libraries
if (!requireNamespace("tikzDevice", quietly = TRUE))
stop("Please install the 'tikzDevice' package.")
if (!requireNamespace("magick", quietly = TRUE))
stop("Please install the 'magick' package.")
library(tikzDevice)
library(magick)
# tikzDevice options
options(tikzLatexPackages = c(
"\\usepackage{tikz}",
"\\usepackage[active,tightpage]{preview}",
"\\PreviewEnvironment{pgfpicture}",
"\\setlength\\PreviewBorder{0pt}",
"\\usepackage{amsmath, amssymb, amsthm, amstext}",
"\\usepackage{bm}"
))
# file paths
tex_file <- file.path(out_dir, paste0(out_name, ".tex"))
pdf_file <- file.path(out_dir, paste0(out_name, ".pdf"))
out_file <- file.path(out_dir, paste0(out_name, ".", out_format))
# tikz file
tikz(tex_file, standAlone = TRUE, width = width, height = height)
eval(plot_expr)
dev.off()
# Compile to PDF
system(
paste(
"cd", out_dir,
"; lualatex -output-directory .",
shQuote(basename(tex_file))
)
)
# Convert the PDF to PNG
image_write(
image_convert(
image_read_pdf(pdf_file),
format = out_format
),
path = out_file,
format = out_format
)
message("Output file created at: ", out_file)
if(cleanup) {
system("rm *.aux; rm *.log; rm *.tex; rm *.pdf")
message("Removed intermediate files.")
}
}
The magick
package is doing the PDF to X conversion internally using ImageMagick, which provides a cross-platform solution that doesn’t depend on Ghostscript being installed.
The above version relies on Unix-style shell commands for changing directories and cleaning up temporary files. While these commands work perfectly on macOS and Linux, they would fail on Windows systems which use different command syntax and path separators: Additionally, Windows handles program execution differently, so the way we call (Lua)LaTeX needs some adjustment.1
Below is cross-platform version of the function that should address these points (untested!).
Code
create_latex_plot <- function(
plot_expr, # plot object / function call
out_name, # out files name
out_format = "png", # out format
out_dir = ".", # out directory
width = 9, # plot width
height = 6, # plot height
cleanup = T # remove intermediate files
) {
# libraries
if (!requireNamespace("tikzDevice", quietly = TRUE))
stop("Please install the 'tikzDevice' package.")
if (!requireNamespace("magick", quietly = TRUE))
stop("Please install the 'magick' package.")
library(tikzDevice)
library(magick)
# tikzDevice options
options(tikzLatexPackages = c(
"\\usepackage{tikz}",
"\\usepackage[active,tightpage]{preview}",
"\\PreviewEnvironment{pgfpicture}",
"\\setlength\\PreviewBorder{0pt}",
"\\usepackage{amsmath, amssymb, amsthm, amstext}",
"\\usepackage{bm}"
))
# file paths
tex_file <- file.path(out_dir, paste0(out_name, ".tex"))
pdf_file <- file.path(out_dir, paste0(out_name, ".pdf"))
out_file <- file.path(out_dir, paste0(out_name, ".", out_format))
# tikz file
tikz(tex_file, standAlone = TRUE, width = width, height = height)
eval(plot_expr)
dev.off()
# current working directory
original_dir <- getwd()
# change directory
setwd(out_dir)
# system2() for cross-platform
system2(
"lualatex",
args = c(
"-interaction=nonstopmode",
"-output-directory=.",
basename(tex_file)
),
stdout = NULL
)
# back to original directory
setwd(original_dir)
# PDF to output conversion
image_write(
image_convert(
image_read_pdf(pdf_file),
format = out_format
),
path = out_file,
format = out_format
)
message("Output file created at: ", out_file)
# cross-platform cleanup
if(cleanup) {
temp_files <- list.files(
path = out_dir,
pattern = paste0(out_name, "\\.(aux|log|tex|pdf)$"),
full.names = TRUE
)
file.remove(temp_files)
message("Removed intermediate files.")
}
}
Example: Density Ridges
To demonstrate the utility of this function, let’s create a ridge plot showing Gaussian density functions with their corresponding mathematical formulas as $\LaTeX$ labels.
We first generate some example data.
library(tibble)
# example data
set.seed(123)
data <- tibble(
value = c(
rnorm(500, mean = 3, sd = 1),
rnorm(500, mean = 5, sd = 1.2),
rnorm(500, mean = 7, sd = 1.5)
),
group = rep(c("Group A", "Group B", "Group C"), each = 500)
)
The plot should display kernel estimates of the corresponding Gaussian densities along with $\LaTeX$ formulas. For this we define the formulas as strings and combine these in a tibble
with positions and group membership indicators.2
# formulas for each group
formulas <- c(
"$f(x) = \\frac{1}{\\sqrt{2\\pi}} e^{-\\frac{(x - 3)^2}{2}}$",
"$f(x) = \\frac{1}{1.2\\sqrt{2\\pi}} e^{-\\frac{(x - 5)^2}{2 \\cdot 1.2^2}}$",
"$f(x) = \\frac{1}{1.5\\sqrt{2\\pi}} e^{-\\frac{(x - 7)^2}{2 \\cdot 1.5^2}}$"
)
# positions for annotations
annotations <- tibble(
group = c("Group A", "Group B", "Group C"),
x = c(6.5, 9, 11),
y = c(1.6, 2.6, 3.6),
formula = formulas
)
We use ggridges::geom_density_ridges
in a ggplot2
call to produce a ridgeline plot of the density estimates and pass annotations
as data input to ggplot2::geom_text()
.
# libraries
library(ggplot2)
library(ggridges)
# create the ridge plot
ridge_plot <- ggplot(
data = data,
mapping = aes(x = value, y = group, fill = group)
) +
geom_density_ridges(scale = 1.5, alpha = 0.8) +
theme_minimal(base_size = 16) +
scale_fill_brewer(palette = "Set2") +
labs(
title = "Density Ridge Plot with Inline \\LaTeX",
x = "Value",
y = "Group"
) +
geom_text(
data = annotations,
aes(x = x, y = y, label = formula),
inherit.aes = FALSE,
size = 5,
color = "black"
) +
theme(
legend.position = "none",
plot.title = element_text(hjust = 0.5),
plot.subtitle = element_text(hjust = 0.5)
)
ridge_plot
Picking joint bandwidth of 0.314
Note that ggplot2
objects are plot specifications that only get rendered when printed.3 However, calling ridge_plot
in R’s plotting device does involve any further typesetting and so we see the bare $\LaTeX$ commands of the annotation layer here.4
This is where create_latex_plot()
works its magic. While printing happens automatically in the R console, the tikzDevice needs an explicit print command. We wrap this command in expression()
to create a set of instructions that can be executed at the right moment inside the tikzDevice
context, rather than immediately. This ensures the plot renders properly with $\LaTeX$ support. For the output file format, I chose png:
# render 'ridge_plot' using create_latex_plot()
create_latex_plot(
plot_expr = expression(print(ridge_plot)),
out_name = "density_ridges",
out_format = "png",
out_dir = ".",
width = 8,
height = 6
)
Linking to ImageMagick 6.9.12.93
Enabled features: cairo, fontconfig, freetype, heic, lcms, pango, raw, rsvg, webp
Disabled features: fftw, ghostscript, x11
Picking joint bandwidth of 0.314
Output file created at: ./density_ridges.png
Removed intermediate files.
And here’s the result:5
That’s it for now 🙂.
-
I guess you could use another $\LaTeX$ compiler, but I did not test this. I had some issues with pdflatex. ↩︎
-
Note that we need to double the backslash
\\
in R strings since\
is an escape character used to denote special characters like newlines (\n
) or tabs (\t
). When the string is processed by LaTeX, a double backslash becomes a single backslash. ↩︎ -
Calling the object
ridge_plot
is equivalent toprint(ridge_plot)
. ↩︎ -
Fomula positions are misleading here: The lengthy expressions are centered at the positions defined in the
annotations
tibble. Rendered $\LaTeX$ formulas often take less space, so better look at the final outcome before adjusting. ↩︎ -
To include the output in an Rmarkdown file or in a quarto document as I do here, you need to embedd the generated file, for example using

. ↩︎