diff --git a/NEWS.md b/NEWS.md
index a8a8347d..a14c6dd0 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -75,6 +75,11 @@ visualizations.
providing finer control for spacing between ticks-labels and labels-titles,
respectively, in dynamic themes. See the **Dynamic themes** entry above.
(#590 @grantmcdermott)
+- New `tinyplot(..., cap =
)` argument for adding a caption to your
+ plots. Captions are drawn at the bottom of the plot and are best paired with
+ dynamic themes (since separation from `sub` is guaranteed). Appearance is
+ customizable via `tpar()` parameters: `adj.cap`, `cex.cap`, `col.cap`,
+ `font.cap`, and `line.cap`. (#592 @grantmcdermott)
### Bug fixes
diff --git a/R/facet.R b/R/facet.R
index b06651a2..795bbf1b 100644
--- a/R/facet.R
+++ b/R/facet.R
@@ -33,6 +33,7 @@ draw_facet_window = function(
has_legend,
main,
sub,
+ cap,
type,
xlab,
x, xmax, xmin,
diff --git a/R/legend.R b/R/legend.R
index 5128ca60..f7fa2df4 100644
--- a/R/legend.R
+++ b/R/legend.R
@@ -169,6 +169,10 @@ legend_outer_margins = function(legend_env, apply = TRUE) {
} else if (legend_env$outer_end) {
if (legend_env$outer_bottom) {
legend_env$ooma[1] = soma
+ if (legend_env$has_cap) {
+ cex_cap = get_tpar("cex.cap", 1)
+ legend_env$ooma[1] = legend_env$ooma[1] + cex_cap + 0.2
+ }
} else {
legend_env$omar[3] = legend_env$omar[3] + soma - legend_env$topmar_epsilon
par(mar = legend_env$omar)
@@ -269,6 +273,10 @@ tinylegend = function(legend_env) {
} else if (legend_env$outer_end) {
if (legend_env$outer_bottom) {
legend_env$ooma[1] = soma
+ if (legend_env$has_cap) {
+ cex_cap = get_tpar("cex.cap", 1)
+ legend_env$ooma[1] = legend_env$ooma[1] + cex_cap + 0.2
+ }
} else {
legend_env$omar[3] = legend_env$omar[3] + soma - legend_env$topmar_epsilon
par(mar = legend_env$omar)
@@ -321,6 +329,21 @@ tinylegend = function(legend_env) {
} else {
do.call("legend", legend_env$args)
}
+
+ if (legend_env$outer_bottom && legend_env$has_cap) {
+ cex_cap = get_tpar("cex.cap", 1)
+ mtext(
+ legend_env$cap_text,
+ side = 1,
+ outer = TRUE,
+ line = par("oma")[1] - 1,
+ cex = cex_cap,
+ col = get_tpar("col.cap", "black"),
+ adj = get_tpar(c("adj.cap", "adj")),
+ font = get_tpar("font.cap", 1),
+ las = 1
+ )
+ }
}
@@ -385,6 +408,7 @@ prepare_legend = function(settings) {
"bubble_cex",
"by",
"by_continuous",
+ "cap",
"cex_dep",
"cex_fct_adj",
"col",
@@ -448,6 +472,7 @@ prepare_legend = function(settings) {
legend_draw_flag = (is.null(legend) || !is.character(legend) || legend != "none" || bubble) && !isTRUE(add)
has_sub = text_line_count(sub) > 0L
+ has_cap = text_line_count(cap) > 0L
# Generate labels for discrete legends
if (legend_draw_flag && isFALSE(by_continuous) && (!bubble || multi_legend)) {
@@ -469,7 +494,8 @@ prepare_legend = function(settings) {
"legend",
"legend_args",
"legend_draw_flag",
- "has_sub"
+ "has_sub",
+ "has_cap"
)
)
}
@@ -711,6 +737,8 @@ build_legend_env = function(
gradient,
lmar,
has_sub = FALSE,
+ has_cap = FALSE,
+ cap_text = NULL,
new_plot = TRUE
) {
# Create legend environment
@@ -720,6 +748,8 @@ build_legend_env = function(
legend_env$gradient = gradient
legend_env$type = type
legend_env$has_sub = has_sub
+ legend_env$has_cap = has_cap
+ legend_env$cap_text = cap_text
legend_env$new_plot = new_plot
legend_env$dynmar = isTRUE(.tpar[["dynmar"]])
legend_env$topmar_epsilon = 0.1
@@ -793,6 +823,11 @@ build_legend_env = function(
#' @param has_sub Logical. Does the plot have a sub-caption. Only used if
#' keyword position is "bottom!", in which case we need to bump the legend
#' margin a bit further.
+#' @param has_cap Logical. Does the plot have a caption. Only used if
+#' keyword position is "bottom!", in which case we need to bump the legend
+#' margin a bit further.
+#' @param cap_text Character. The caption text to draw below the legend when
+#' position is "bottom!". Ignored otherwise.
#' @param new_plot Logical. Should we be calling plot.new internally?
#' @param draw Logical. If `FALSE`, no legend is drawn but the sizes are
#' returned. Note that a new (blank) plot frame will still need to be started
@@ -881,6 +916,8 @@ draw_legend = function(
gradient = FALSE,
lmar = NULL,
has_sub = FALSE,
+ has_cap = FALSE,
+ cap_text = NULL,
new_plot = TRUE,
draw = TRUE,
soma_target = NULL
@@ -895,6 +932,7 @@ draw_legend = function(
assert_logical(gradient)
assert_logical(has_sub)
+ assert_logical(has_cap)
assert_logical(new_plot)
assert_logical(draw)
@@ -929,6 +967,8 @@ draw_legend = function(
gradient = gradient,
lmar = lmar,
has_sub = has_sub,
+ has_cap = has_cap,
+ cap_text = cap_text,
new_plot = new_plot
)
diff --git a/R/tinyplot.R b/R/tinyplot.R
index 0362f825..289d470a 100644
--- a/R/tinyplot.R
+++ b/R/tinyplot.R
@@ -167,6 +167,12 @@
#' legend arguments, e.g. "bty", "horiz", and so forth.
#' @param main a main title for the plot, see also `title`.
#' @param sub a subtitle for the plot.
+#' @param cap a caption for the plot, drawn at the bottom. Useful for
+#' annotations like data sources. Best paired with a dynamic
+#' \code{\link[tinyplot]{tinytheme}}. For the default theme, should be seen as
+#' a substitute for `sub`, since these two will otherwise overlap. Appearance
+#' can be customized via \code{\link[tinyplot]{tpar}} parameters `adj.cap`,
+#' `cex.cap`, `col.cap`, `font.cap`, and `line.cap`.
#' @param xlab a label for the x axis, defaults to a description of x.
#' @param ylab a label for the y axis, defaults to a description of y.
#' @param ann a logical value indicating whether the default annotation (title
@@ -609,17 +615,16 @@
#' # parameters (e.g., via `(t)par`)... But a more convenient way is to just use
#' # built-in themes (see `?tinytheme`).
#'
-#' tinytheme("clean2")
#' tinyplot(
#' Temp ~ Day | Month,
#' data = aq,
#' type = "b",
#' alpha = 0.5,
#' main = "Daily temperatures by month",
-#' sub = "Brought to you by tinyplot"
+#' sub = "Brought to you by tinyplot",
+#' cap = "Source: Base R airquality dataset",
+#' theme = "clean2"
#' )
-#' # reset the theme
-#' tinytheme()
#'
#' # For more examples and a detailed walkthrough, please see the introductory
#' # tinyplot tutorial available online:
@@ -649,6 +654,7 @@ tinyplot.default = function(
legend = NULL,
main = NULL,
sub = NULL,
+ cap = NULL,
xlab = NULL,
ylab = NULL,
ann = par("ann"),
@@ -855,11 +861,13 @@ tinyplot.default = function(
# misc
add = add,
by = by,
+ cap = cap,
dodge = NULL,
dots = dots,
flip = flip,
group_offsets = NULL,
offsets_axis = NULL,
+ sub = sub,
type_info = list() # pass type-specific info from type_data to type_draw
)
@@ -1052,7 +1060,9 @@ tinyplot.default = function(
)
.dyn = c(
- dynmar_side(1, xlab, main = main, sub = sub, side.sub = .side.sub,
+ dynmar_side(1, xlab, main = main, sub = sub,
+ cap = if (.outer_sides[1]) NULL else cap,
+ side.sub = .side.sub,
axis_on = !identical(xaxt, "none") && !identical(xaxt, "n"),
tpars = .tpars),
dynmar_side(2, ylab,
@@ -1136,7 +1146,9 @@ tinyplot.default = function(
bg = bg,
gradient = by_continuous,
cex = lgnd_cex,
- has_sub = has_sub
+ has_sub = has_sub,
+ has_cap = has_cap,
+ cap_text = cap
)
} else {
## multi-legend case...
@@ -1210,7 +1222,7 @@ tinyplot.default = function(
}
}
- draw_title(main, sub, xlab, ylab, legend, legend_args, opar,
+ draw_title(main, sub, cap, xlab, ylab, legend, legend_args, opar,
xlab_line_offset = if (!is.null(dynmar_computed)) .whtsbp[1] else 0,
ylab_line_offset = if (!is.null(dynmar_computed)) .whtsbp[2] - .ymgp_shift - .ylab_cex_shift else 0)
}
@@ -1294,6 +1306,7 @@ tinyplot.default = function(
has_legend = has_legend,
main = main,
sub = sub,
+ cap = cap,
type = type,
xlab = xlab,
x = x, xmax = xmax, xmin = xmin,
@@ -1324,6 +1337,7 @@ tinyplot.default = function(
has_legend = has_legend,
main = main,
sub = sub,
+ cap = cap,
type = type,
xlab = xlab,
x = datapoints$x, xmax = datapoints$xmax, xmin = datapoints$xmin,
@@ -1594,6 +1608,7 @@ tinyplot.formula = function(
# log = "",
main = NULL,
sub = NULL,
+ cap = NULL,
xlab = NULL,
ylab = NULL,
ann = par("ann"),
@@ -1742,6 +1757,7 @@ tinyplot.formula = function(
# log = "",
main = main,
sub = sub,
+ cap = cap,
xlab = xlab,
ylab = ylab,
ann = ann,
diff --git a/R/tinytheme.R b/R/tinytheme.R
index 4ec7b3e3..5795eee7 100644
--- a/R/tinytheme.R
+++ b/R/tinytheme.R
@@ -162,7 +162,8 @@
#' I(Sepal.Length*1e4) ~ Petal.Length | Species, facet = "by", data = iris,
#' yaxl = ",",
#' main = paste0('tinytheme("', thm, '")'),
-#' sub = "A subtitle"
+#' sub = "A subtitle",
+#' cap = "A caption"
#' )
#' box("outer", lty = 2)
#' }
@@ -267,6 +268,7 @@ theme_default = list(
tinytheme = "default",
adj = par("adj"), # 0.5,
adj.main = par("adj"), # 0.5,
+ adj.cap = par("adj"), # 0.5,
adj.sub = par("adj"), # 0.5,
bg = "white", # par("bg") # "white"
bty = par("bty"), #"o",
@@ -274,6 +276,7 @@ theme_default = list(
cex.axis = par("cex.axis"), #1,
cex.lab = par("cex.lab"), #1,
cex.main = par("cex.main"), #1.2,
+ cex.cap = 1,
cex.sub = par("cex.sub"), #1,
cex.xlab = NULL, # defer to par("cex.lab") unless set explicitly
cex.ylab = NULL, # defer to par("cex.lab") unless set explicitly
@@ -283,6 +286,7 @@ theme_default = list(
col.yaxs = par("col.axis"), #1,
col.lab = par("col.lab"), #"black",
col.main = par("col.main"), #"black",
+ col.cap = "black",
col.sub = par("col.sub"), #"black",
dynmar = FALSE,
facet.bg = NULL,
@@ -293,6 +297,7 @@ theme_default = list(
font.axis = par("font.axis"), # 1,
font.lab = par("font.lab"), # 1,
font.main = par("font.main"), # 2,
+ font.cap = 1,
font.sub = par("font.sub"), # 2,
grid = FALSE,
grid.col = "lightgray",
@@ -322,6 +327,7 @@ theme_default = list(
theme_basic = modifyList(theme_default, list(
tinytheme = "basic",
+ adj.cap = 1,
facet.bg = "gray90",
facet.border = "black",
grid = TRUE,
@@ -330,6 +336,7 @@ theme_basic = modifyList(theme_default, list(
theme_tufte = modifyList(theme_default, list(
tinytheme = "tufte",
+ adj.cap = 1,
adj.main = 0,
adj.sub = 0,
bty = "n",
@@ -343,6 +350,7 @@ theme_tufte = modifyList(theme_default, list(
theme_void = modifyList(theme_default, list(
tinytheme = "void",
+ adj.cap = 1,
adj.main = 0,
adj.sub = 0,
font.main = 1,
@@ -391,6 +399,7 @@ theme_dynamic = modifyList(theme_basic, list(
theme_clean = modifyList(theme_dynamic, list(
tinytheme = "clean",
+ cex.cap = 0.8,
grid = TRUE,
palette.qualitative = "Tableau 10",
palette.sequential = "ag_Sunset"
@@ -399,6 +408,7 @@ theme_clean = modifyList(theme_dynamic, list(
theme_classic = modifyList(theme_dynamic, list(
tinytheme = "classic",
bty = "l",
+ cex.cap = 0.8,
facet.bg = NULL,
font.main = 1,
palette.qualitative = "Okabe-Ito"
@@ -456,6 +466,7 @@ theme_dark = modifyList(theme_minimal, list(
col.yaxs = "#BBBBBB",
col.lab = "#BBBBBB",
col.main = "#BBBBBB",
+ col.cap = "#BBBBBB",
col.sub = "#BBBBBB",
col.axis = "#BBBBBB",
# facet.bg = "gray20",
diff --git a/R/title.R b/R/title.R
index 5dffd555..2992240e 100644
--- a/R/title.R
+++ b/R/title.R
@@ -1,4 +1,4 @@
-draw_title = function(main, sub, xlab, ylab, legend, legend_args, opar,
+draw_title = function(main, sub, cap, xlab, ylab, legend, legend_args, opar,
xlab_line_offset = 0,
ylab_line_offset = 0) {
# main title
@@ -79,6 +79,27 @@ draw_title = function(main, sub, xlab, ylab, legend, legend_args, opar,
}
+ cap_in_legend = !is.null(legend_args[["x"]]) && grepl("bottom!$", legend_args[["x"]])
+ if (!is.null(cap) && !cap_in_legend) {
+ cex_cap = get_tpar("cex.cap", 1)
+ line_cap = get_tpar("line.cap", NULL)
+ if (is.null(line_cap)) {
+ line_cap = par("mar")[1] - 1
+ }
+ args = list(
+ text = cap,
+ line = line_cap,
+ cex = cex_cap,
+ col = get_tpar("col.cap", "black"),
+ adj = get_tpar(c("adj.cap", "adj")),
+ font = get_tpar("font.cap", 1),
+ side = 1,
+ las = 1
+ )
+ args = Filter(function(x) !is.null(x), args)
+ do.call(mtext, args)
+ }
+
# Axis titles. For multi-line labels, base R places line 1 at
# `line = mgp[1] - (N-1)*cex`, which pushes line 1 up into the tick-label
# zone. Shift `line` down so line 1 aligns with where a single-line xlab
diff --git a/R/tpar.R b/R/tpar.R
index 5479791c..e86757e3 100644
--- a/R/tpar.R
+++ b/R/tpar.R
@@ -49,9 +49,14 @@
#'
#' @section Additional Graphical Parameters:
#'
+#' * `adj.cap`: Numeric value between 0 and 1 controlling the alignment of the plot caption. Defaults to `0.5` (centered) for the default theme, and `1` (right-aligned) for all other themes.
#' * `adj.xlab`: Numeric value between 0 and 1 controlling the alignment of the x-axis label.
#' * `adj.ylab`: Numeric value between 0 and 1 controlling the alignment of the y-axis label.
#' * `cairo`: Logical indicating whether \code{\link[grDevices]{cairo_pdf}} should be used when writing plots to PDF. If `FALSE`, then \code{\link[grDevices]{pdf}} will be used instead, with implications for embedding (non-standard) fonts. Only used if `tinyplot(..., file = ".pdf")` is called. Defaults to the value of `capabilities("cairo")`.
+#' * `cex.cap`: Numeric expansion factor for the plot caption text. Defaults to `1` for the default, basic, and dynamic themes, and `0.8` for clean/classic and their descendants.
+#' * `col.cap`: Character specifying the colour of the plot caption. Defaults to `"black"`.
+#' * `font.cap`: Integer specifying the font face for the plot caption (`1` = plain, `2` = bold, `3` = italic, `4` = bold italic). Defaults to `1`.
+#' * `line.cap`: Numeric specifying the margin line on which to draw the caption. If `NULL` (default), computed automatically based on the available bottom margin.
#' * `dynmar`: Logical indicating whether `tinyplot` should attempt dynamic adjustment of margins to reduce whitespace and/or account for spacing of text elements (e.g., long horizontal y-axis labels). Note that this parameter is tightly coupled to internal `tinythemes()` logic and should _not_ be adjusted manually unless you really know what you are doing or don't mind risking unintended consequences to your plot.
#' * `facet.bg`: Character or integer specifying the facet background colour. If an integer, will correspond to the user's default colour palette (see `palette`). Passed to `rect`. Defaults to `NULL` (none).
#' * `facet.border`: Character or integer specifying the facet border colour. If an integer, will correspond to the user's default colour palette (see `palette`). Passed to `rect`. Defaults to `NA` (none).
@@ -218,16 +223,20 @@ get_tpar = function(opts, default = NULL, tpar_list = NULL) {
known_tpar = c(
+ "adj.cap",
"adj.main",
"adj.sub",
"adj.xlab",
"adj.ylab",
+ "cex.cap",
"cex.xlab",
"cex.ylab",
+ "col.cap",
"col.xaxs",
"col.yaxs",
"cairo",
"dynmar",
+ "font.cap",
"gap.axis",
"gap.lab",
"facet.bg",
@@ -244,6 +253,7 @@ known_tpar = c(
"grid.col",
"grid.lty",
"grid.lwd",
+ "line.cap",
"ljust",
"lmar",
"lty.xaxs",
@@ -270,6 +280,7 @@ assign_tpar = function(opts) {
assert_tpar = function(.tpar) {
+ assert_numeric(.tpar[["adj.cap"]], len = 1, lower = 0, upper = 1, null.ok = TRUE, name = "adj.cap")
assert_numeric(.tpar[["adj.main"]], len = 1, lower = 0, upper = 1, null.ok = TRUE, name = "adj.main")
assert_numeric(.tpar[["adj.sub"]], len = 1, lower = 0, upper = 1, null.ok = TRUE, name = "adj.sub")
assert_numeric(.tpar[["adj.xlab"]], len = 1, lower = 0, upper = 1, null.ok = TRUE, name = "adj.xlab")
diff --git a/R/utils.R b/R/utils.R
index c89a7aa0..fe0f7025 100644
--- a/R/utils.R
+++ b/R/utils.R
@@ -31,7 +31,7 @@ text_line_count = function(x) {
# handled separately by the existing whtsbp logic in draw_facet_window().
# The caller is expected to take max(theme_mar[side], dynmar_side(...)) so
# that the theme's starting `mar` acts as a baseline padding.
-dynmar_side = function(side, label, main = NULL, sub = NULL,
+dynmar_side = function(side, label, main = NULL, sub = NULL, cap = NULL,
side.sub = 3, axis_on = TRUE, tpars = NULL) {
mgp = get_tpar("mgp", tpar_list = tpars)
tcl = get_tpar("tcl", tpar_list = tpars, default = par("tcl"))
@@ -89,6 +89,11 @@ dynmar_side = function(side, label, main = NULL, sub = NULL,
asc = if (has_main_here) 0 else 0.6 * cex_sub
mar = mar + (cex_sub + 0.2) + (slines - 1) * cex_sub + asc
}
+ clines = text_line_count(cap)
+ if (clines >= 1L && side == 1L) {
+ cex_cap = get_tpar("cex.cap", tpar_list = tpars, default = 1)
+ mar = mar + (cex_cap + 0.2) + (clines - 1) * cex_cap
+ }
mar
}
diff --git a/R/zzz.R b/R/zzz.R
index 730dcd31..9eaa226d 100644
--- a/R/zzz.R
+++ b/R/zzz.R
@@ -22,6 +22,7 @@
"by_continuous",
"by_dep",
"by_ordered",
+ "cap",
"cex",
"cex_dep",
"cex_fct_adj",
@@ -62,6 +63,7 @@
"legend",
"lgnd_labs",
"lgnd_cex",
+ "has_cap",
"has_sub",
"legend_draw_flag",
"multi_legend",
diff --git a/altdoc/pkgdown.yml b/altdoc/pkgdown.yml
index 0758a82b..8a4d3e90 100644
--- a/altdoc/pkgdown.yml
+++ b/altdoc/pkgdown.yml
@@ -2,7 +2,7 @@ altdoc: 0.7.2
pandoc: 3.9.0.2
pkgdown: 2.1.3
pkgdown_sha: ~
-last_built: 2026-05-18T20:58:22+0000
+last_built: 2026-05-20T20:09:08+0000
urls:
reference: https://grantmcdermott.com/tinyplot/man
article: https://grantmcdermott.com/tinyplot/vignettes
diff --git a/inst/tinytest/_tinysnapshot/cap_bottom_legend.svg b/inst/tinytest/_tinysnapshot/cap_bottom_legend.svg
new file mode 100644
index 00000000..8ece9600
--- /dev/null
+++ b/inst/tinytest/_tinysnapshot/cap_bottom_legend.svg
@@ -0,0 +1,255 @@
+
+
diff --git a/inst/tinytest/_tinysnapshot/cap_clean.svg b/inst/tinytest/_tinysnapshot/cap_clean.svg
new file mode 100644
index 00000000..84c1b269
--- /dev/null
+++ b/inst/tinytest/_tinysnapshot/cap_clean.svg
@@ -0,0 +1,255 @@
+
+
diff --git a/inst/tinytest/_tinysnapshot/cap_default_theme.svg b/inst/tinytest/_tinysnapshot/cap_default_theme.svg
new file mode 100644
index 00000000..cbf19746
--- /dev/null
+++ b/inst/tinytest/_tinysnapshot/cap_default_theme.svg
@@ -0,0 +1,226 @@
+
+
diff --git a/inst/tinytest/test-cap.R b/inst/tinytest/test-cap.R
new file mode 100644
index 00000000..4d39b79d
--- /dev/null
+++ b/inst/tinytest/test-cap.R
@@ -0,0 +1,24 @@
+source("helpers.R")
+using("tinysnapshot")
+
+f = function() {
+ tinyplot(Sepal.Length ~ Petal.Length | Species, data = iris,
+ theme = "clean",
+ cap = "Source: Anderson (1935)")
+}
+expect_snapshot_plot(f, label = "cap_clean")
+
+f = function() {
+ tinyplot(Sepal.Length ~ Petal.Length | Species, data = iris,
+ legend = "bottom!",
+ theme = "clean",
+ cap = "Source: Anderson (1935)")
+}
+expect_snapshot_plot(f, label = "cap_bottom_legend")
+
+f = function() {
+ tinyplot(Sepal.Length ~ Petal.Length, data = iris,
+ main = "`sub` and `cap` overlap for default theme",
+ sub = "Subtitle", cap = "Caption")
+}
+expect_snapshot_plot(f, label = "cap_default_theme")
diff --git a/man/build_legend_env.Rd b/man/build_legend_env.Rd
index e4b2cccb..3cebf653 100644
--- a/man/build_legend_env.Rd
+++ b/man/build_legend_env.Rd
@@ -20,6 +20,8 @@ build_legend_env(
gradient,
lmar,
has_sub = FALSE,
+ has_cap = FALSE,
+ cap_text = NULL,
new_plot = TRUE
)
}
diff --git a/man/draw_legend.Rd b/man/draw_legend.Rd
index 971dd523..ee2e73fe 100644
--- a/man/draw_legend.Rd
+++ b/man/draw_legend.Rd
@@ -20,6 +20,8 @@ draw_legend(
gradient = FALSE,
lmar = NULL,
has_sub = FALSE,
+ has_cap = FALSE,
+ cap_text = NULL,
new_plot = TRUE,
draw = TRUE,
soma_target = NULL
@@ -66,6 +68,13 @@ for which the default values are \code{c(1.0, 0.1)}.}
keyword position is "bottom!", in which case we need to bump the legend
margin a bit further.}
+\item{has_cap}{Logical. Does the plot have a caption. Only used if
+keyword position is "bottom!", in which case we need to bump the legend
+margin a bit further.}
+
+\item{cap_text}{Character. The caption text to draw below the legend when
+position is "bottom!". Ignored otherwise.}
+
\item{new_plot}{Logical. Should we be calling plot.new internally?}
\item{draw}{Logical. If \code{FALSE}, no legend is drawn but the sizes are
diff --git a/man/facet.Rd b/man/facet.Rd
index 8799e439..1be2b97f 100644
--- a/man/facet.Rd
+++ b/man/facet.Rd
@@ -49,6 +49,7 @@ draw_facet_window(
has_legend,
main,
sub,
+ cap,
type,
xlab,
x,
diff --git a/man/tinyplot.Rd b/man/tinyplot.Rd
index 252c8de4..ddd49c5c 100644
--- a/man/tinyplot.Rd
+++ b/man/tinyplot.Rd
@@ -25,6 +25,7 @@ tinyplot(x, ...)
legend = NULL,
main = NULL,
sub = NULL,
+ cap = NULL,
xlab = NULL,
ylab = NULL,
ann = par("ann"),
@@ -78,6 +79,7 @@ tinyplot(x, ...)
ylim = NULL,
main = NULL,
sub = NULL,
+ cap = NULL,
xlab = NULL,
ylab = NULL,
ann = par("ann"),
@@ -292,6 +294,13 @@ legend arguments, e.g. "bty", "horiz", and so forth.
\item{sub}{a subtitle for the plot.}
+\item{cap}{a caption for the plot, drawn at the bottom. Useful for
+annotations like data sources. Best paired with a dynamic
+\code{\link[tinyplot]{tinytheme}}. For the default theme, should be seen as
+a substitute for \code{sub}, since these two will otherwise overlap. Appearance
+can be customized via \code{\link[tinyplot]{tpar}} parameters \code{adj.cap},
+\code{cex.cap}, \code{col.cap}, \code{font.cap}, and \code{line.cap}.}
+
\item{xlab}{a label for the x axis, defaults to a description of x.}
\item{ylab}{a label for the y axis, defaults to a description of y.}
@@ -779,17 +788,16 @@ tinyplot(
# parameters (e.g., via `(t)par`)... But a more convenient way is to just use
# built-in themes (see `?tinytheme`).
-tinytheme("clean2")
tinyplot(
Temp ~ Day | Month,
data = aq,
type = "b",
alpha = 0.5,
main = "Daily temperatures by month",
- sub = "Brought to you by tinyplot"
+ sub = "Brought to you by tinyplot",
+ cap = "Source: Base R airquality dataset",
+ theme = "clean2"
)
-# reset the theme
-tinytheme()
# For more examples and a detailed walkthrough, please see the introductory
# tinyplot tutorial available online:
diff --git a/man/tinytheme.Rd b/man/tinytheme.Rd
index 8b54c1d0..f8fb5703 100644
--- a/man/tinytheme.Rd
+++ b/man/tinytheme.Rd
@@ -172,7 +172,8 @@ for (thm in thms) {
I(Sepal.Length*1e4) ~ Petal.Length | Species, facet = "by", data = iris,
yaxl = ",",
main = paste0('tinytheme("', thm, '")'),
- sub = "A subtitle"
+ sub = "A subtitle",
+ cap = "A caption"
)
box("outer", lty = 2)
}
diff --git a/man/tpar.Rd b/man/tpar.Rd
index eea89829..8cf705ca 100644
--- a/man/tpar.Rd
+++ b/man/tpar.Rd
@@ -60,9 +60,14 @@ you should rather use \code{par()} instead.
\section{Additional Graphical Parameters}{
\itemize{
+\item \code{adj.cap}: Numeric value between 0 and 1 controlling the alignment of the plot caption. Defaults to \code{0.5} (centered) for the default theme, and \code{1} (right-aligned) for all other themes.
\item \code{adj.xlab}: Numeric value between 0 and 1 controlling the alignment of the x-axis label.
\item \code{adj.ylab}: Numeric value between 0 and 1 controlling the alignment of the y-axis label.
\item \code{cairo}: Logical indicating whether \code{\link[grDevices]{cairo_pdf}} should be used when writing plots to PDF. If \code{FALSE}, then \code{\link[grDevices]{pdf}} will be used instead, with implications for embedding (non-standard) fonts. Only used if \code{tinyplot(..., file = ".pdf")} is called. Defaults to the value of \code{capabilities("cairo")}.
+\item \code{cex.cap}: Numeric expansion factor for the plot caption text. Defaults to \code{1} for the default, basic, and dynamic themes, and \code{0.8} for clean/classic and their descendants.
+\item \code{col.cap}: Character specifying the colour of the plot caption. Defaults to \code{"black"}.
+\item \code{font.cap}: Integer specifying the font face for the plot caption (\code{1} = plain, \code{2} = bold, \code{3} = italic, \code{4} = bold italic). Defaults to \code{1}.
+\item \code{line.cap}: Numeric specifying the margin line on which to draw the caption. If \code{NULL} (default), computed automatically based on the available bottom margin.
\item \code{dynmar}: Logical indicating whether \code{tinyplot} should attempt dynamic adjustment of margins to reduce whitespace and/or account for spacing of text elements (e.g., long horizontal y-axis labels). Note that this parameter is tightly coupled to internal \code{tinythemes()} logic and should \emph{not} be adjusted manually unless you really know what you are doing or don't mind risking unintended consequences to your plot.
\item \code{facet.bg}: Character or integer specifying the facet background colour. If an integer, will correspond to the user's default colour palette (see \code{palette}). Passed to \code{rect}. Defaults to \code{NULL} (none).
\item \code{facet.border}: Character or integer specifying the facet border colour. If an integer, will correspond to the user's default colour palette (see \code{palette}). Passed to \code{rect}. Defaults to \code{NA} (none).
diff --git a/vignettes/introduction.qmd b/vignettes/introduction.qmd
index 291caf10..7ab6111d 100644
--- a/vignettes/introduction.qmd
+++ b/vignettes/introduction.qmd
@@ -564,7 +564,8 @@ tinytheme("dark")
tinyplot(
Temp ~ Wind | Ozone, data = aq,
main = "An example of a tinytheme() in action",
- sub = "Notice that the subtitle is above the plot"
+ sub = "Notice that the subtitle is above the plot",
+ cap = "Source: A helpful caption"
)
```
```{r}
diff --git a/vignettes/themes.qmd b/vignettes/themes.qmd
index a3bdb563..975fa80c 100644
--- a/vignettes/themes.qmd
+++ b/vignettes/themes.qmd
@@ -52,7 +52,8 @@ tinyplot(
facet = "by",
data = iris,
main = "Title of the plot",
- sub = "A smaller subtitle"
+ sub = "A smaller subtitle",
+ cap = "Source: A helpful caption"
)
```
@@ -68,7 +69,8 @@ tinyplot(
data = iris,
yaxl = ",", # use comma format for the y-axis labels
main = "Title of the plot",
- sub = "The left-margin adjusts to accomodate the long y-axis labels"
+ sub = "A smaller subtitle",
+ cap = "Note: The left-margin adjusts to accomodate the long y-axis labels"
)
```
@@ -118,6 +120,7 @@ p = function(theme = "default") {
data = iris,
main = paste0('theme = "', theme, '"'),
sub = "subtitle",
+ cap = "caption",
theme = theme
)
box("outer", lty = 2)
@@ -125,6 +128,12 @@ p = function(theme = "default") {
```
+:::{.callout-note}
+For non-dynamic themes, the `sub` and `cap` arguments are effectively
+subsitutes, and so will clash for these cases below. But they achieve guaranteed
+separate placement for dynamic themes.
+:::
+
@@ -172,6 +181,7 @@ p2 = function(theme = "ridge") {
type = "ridge",
main = paste0('theme = "', theme, '"'),
sub = "subtitle",
+ cap = "caption",
theme = theme
)
box("outer", lty = 2)
@@ -246,10 +256,13 @@ maintaining constant visible spacing regardless of text size. For example:
```{r}
#| layout-ncol: 2
tinytheme("dynamic", gap.axis = 0, gap.lab = 0.5)
-tinyplot(mpg ~ hp, data = mtcars, main = "Tighter gaps")
+tinyplot(mpg ~ hp, data = mtcars, main = "Tighter axis gaps")
+box("outer", lty = 2)
tinytheme("dynamic", gap.axis = 2, gap.lab = 2)
-tinyplot(mpg ~ hp, data = mtcars, main = "Looser gaps")
+tinyplot(mpg ~ hp, data = mtcars, main = "Looser axis gaps")
+box("outer", lty = 2)
+
tinytheme() # reset
```