From 7e47cbd892ddab4c928c8f7073882e411d2e4fd1 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Tue, 19 May 2026 22:05:30 -0700 Subject: [PATCH 1/6] working prototype --- R/facet.R | 1 + R/legend.R | 40 +++++++++++++++++++++++++++++++++++++++- R/tinyplot.R | 21 ++++++++++++++++++--- R/tinytheme.R | 10 ++++++++++ R/title.R | 23 ++++++++++++++++++++++- R/tpar.R | 6 ++++++ R/utils.R | 7 ++++++- man/build_legend_env.Rd | 2 ++ man/draw_legend.Rd | 6 ++++++ man/facet.Rd | 1 + man/tinyplot.Rd | 7 +++++++ 11 files changed, 118 insertions(+), 6 deletions(-) diff --git a/R/facet.R b/R/facet.R index b06651a2..1aa8e4ff 100644 --- a/R/facet.R +++ b/R/facet.R @@ -33,6 +33,7 @@ draw_facet_window = function( has_legend, main, sub, + caption, type, xlab, x, xmax, xmin, diff --git a/R/legend.R b/R/legend.R index 5128ca60..b8e59a2e 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_caption) { + cex_cap = get_tpar("cex.caption", 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_caption) { + cex_cap = get_tpar("cex.caption", 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_caption) { + cex_cap = get_tpar("cex.caption", 1) + mtext( + legend_env$caption_text, + side = 1, + outer = TRUE, + line = par("oma")[1] - 1, + cex = cex_cap, + col = get_tpar("col.caption", "black"), + adj = get_tpar(c("adj.caption", "adj")), + font = get_tpar("font.caption", 1), + las = 1 + ) + } } @@ -385,6 +408,7 @@ prepare_legend = function(settings) { "bubble_cex", "by", "by_continuous", + "caption", "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_caption = text_line_count(caption) > 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_caption" ) ) } @@ -711,6 +737,8 @@ build_legend_env = function( gradient, lmar, has_sub = FALSE, + has_caption = FALSE, + caption_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_caption = has_caption + legend_env$caption_text = caption_text legend_env$new_plot = new_plot legend_env$dynmar = isTRUE(.tpar[["dynmar"]]) legend_env$topmar_epsilon = 0.1 @@ -793,6 +823,9 @@ 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_caption 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 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 +914,8 @@ draw_legend = function( gradient = FALSE, lmar = NULL, has_sub = FALSE, + has_caption = FALSE, + caption_text = NULL, new_plot = TRUE, draw = TRUE, soma_target = NULL @@ -895,6 +930,7 @@ draw_legend = function( assert_logical(gradient) assert_logical(has_sub) + assert_logical(has_caption) assert_logical(new_plot) assert_logical(draw) @@ -929,6 +965,8 @@ draw_legend = function( gradient = gradient, lmar = lmar, has_sub = has_sub, + has_caption = has_caption, + caption_text = caption_text, new_plot = new_plot ) diff --git a/R/tinyplot.R b/R/tinyplot.R index 0362f825..a2e2cd03 100644 --- a/R/tinyplot.R +++ b/R/tinyplot.R @@ -167,6 +167,10 @@ #' 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 caption a caption for the plot, drawn at the bottom-right. Useful for +#' annotations like data sources. Appearance can be customized via +#' \code{\link[tinyplot]{tpar}} parameters `adj.caption`, `cex.caption`, +#' `col.caption`, `font.caption`, and `line.caption`. #' @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 @@ -649,6 +653,7 @@ tinyplot.default = function( legend = NULL, main = NULL, sub = NULL, + caption = NULL, xlab = NULL, ylab = NULL, ann = par("ann"), @@ -855,11 +860,13 @@ tinyplot.default = function( # misc add = add, by = by, + caption = caption, 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 +1059,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, + caption = if (.outer_sides[1]) NULL else caption, + side.sub = .side.sub, axis_on = !identical(xaxt, "none") && !identical(xaxt, "n"), tpars = .tpars), dynmar_side(2, ylab, @@ -1136,7 +1145,9 @@ tinyplot.default = function( bg = bg, gradient = by_continuous, cex = lgnd_cex, - has_sub = has_sub + has_sub = has_sub, + has_caption = has_caption, + caption_text = caption ) } else { ## multi-legend case... @@ -1210,7 +1221,7 @@ tinyplot.default = function( } } - draw_title(main, sub, xlab, ylab, legend, legend_args, opar, + draw_title(main, sub, caption, 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 +1305,7 @@ tinyplot.default = function( has_legend = has_legend, main = main, sub = sub, + caption = caption, type = type, xlab = xlab, x = x, xmax = xmax, xmin = xmin, @@ -1324,6 +1336,7 @@ tinyplot.default = function( has_legend = has_legend, main = main, sub = sub, + caption = caption, type = type, xlab = xlab, x = datapoints$x, xmax = datapoints$xmax, xmin = datapoints$xmin, @@ -1594,6 +1607,7 @@ tinyplot.formula = function( # log = "", main = NULL, sub = NULL, + caption = NULL, xlab = NULL, ylab = NULL, ann = par("ann"), @@ -1742,6 +1756,7 @@ tinyplot.formula = function( # log = "", main = main, sub = sub, + caption = caption, xlab = xlab, ylab = ylab, ann = ann, diff --git a/R/tinytheme.R b/R/tinytheme.R index 4ec7b3e3..d5f98691 100644 --- a/R/tinytheme.R +++ b/R/tinytheme.R @@ -267,6 +267,7 @@ theme_default = list( tinytheme = "default", adj = par("adj"), # 0.5, adj.main = par("adj"), # 0.5, + adj.caption = par("adj"), # 0.5, adj.sub = par("adj"), # 0.5, bg = "white", # par("bg") # "white" bty = par("bty"), #"o", @@ -274,6 +275,7 @@ theme_default = list( cex.axis = par("cex.axis"), #1, cex.lab = par("cex.lab"), #1, cex.main = par("cex.main"), #1.2, + cex.caption = 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 +285,7 @@ theme_default = list( col.yaxs = par("col.axis"), #1, col.lab = par("col.lab"), #"black", col.main = par("col.main"), #"black", + col.caption = "black", col.sub = par("col.sub"), #"black", dynmar = FALSE, facet.bg = NULL, @@ -293,6 +296,7 @@ theme_default = list( font.axis = par("font.axis"), # 1, font.lab = par("font.lab"), # 1, font.main = par("font.main"), # 2, + font.caption = 1, font.sub = par("font.sub"), # 2, grid = FALSE, grid.col = "lightgray", @@ -322,6 +326,7 @@ theme_default = list( theme_basic = modifyList(theme_default, list( tinytheme = "basic", + adj.caption = 1, facet.bg = "gray90", facet.border = "black", grid = TRUE, @@ -330,6 +335,7 @@ theme_basic = modifyList(theme_default, list( theme_tufte = modifyList(theme_default, list( tinytheme = "tufte", + adj.caption = 1, adj.main = 0, adj.sub = 0, bty = "n", @@ -343,6 +349,7 @@ theme_tufte = modifyList(theme_default, list( theme_void = modifyList(theme_default, list( tinytheme = "void", + adj.caption = 1, adj.main = 0, adj.sub = 0, font.main = 1, @@ -391,6 +398,7 @@ theme_dynamic = modifyList(theme_basic, list( theme_clean = modifyList(theme_dynamic, list( tinytheme = "clean", + cex.caption = 0.8, grid = TRUE, palette.qualitative = "Tableau 10", palette.sequential = "ag_Sunset" @@ -399,6 +407,7 @@ theme_clean = modifyList(theme_dynamic, list( theme_classic = modifyList(theme_dynamic, list( tinytheme = "classic", bty = "l", + cex.caption = 0.8, facet.bg = NULL, font.main = 1, palette.qualitative = "Okabe-Ito" @@ -456,6 +465,7 @@ theme_dark = modifyList(theme_minimal, list( col.yaxs = "#BBBBBB", col.lab = "#BBBBBB", col.main = "#BBBBBB", + col.caption = "#888888", col.sub = "#BBBBBB", col.axis = "#BBBBBB", # facet.bg = "gray20", diff --git a/R/title.R b/R/title.R index 5dffd555..0d00c783 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, caption, 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, } + caption_in_legend = !is.null(legend_args[["x"]]) && grepl("bottom!$", legend_args[["x"]]) + if (!is.null(caption) && !caption_in_legend) { + cex_cap = get_tpar("cex.caption", 1) + line_caption = get_tpar("line.caption", NULL) + if (is.null(line_caption)) { + line_caption = par("mar")[1] - 1 + } + args = list( + text = caption, + line = line_caption, + cex = cex_cap, + col = get_tpar("col.caption", "black"), + adj = get_tpar(c("adj.caption", "adj")), + font = get_tpar("font.caption", 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..831d2c45 100644 --- a/R/tpar.R +++ b/R/tpar.R @@ -218,16 +218,20 @@ get_tpar = function(opts, default = NULL, tpar_list = NULL) { known_tpar = c( + "adj.caption", "adj.main", "adj.sub", "adj.xlab", "adj.ylab", + "cex.caption", "cex.xlab", "cex.ylab", + "col.caption", "col.xaxs", "col.yaxs", "cairo", "dynmar", + "font.caption", "gap.axis", "gap.lab", "facet.bg", @@ -244,6 +248,7 @@ known_tpar = c( "grid.col", "grid.lty", "grid.lwd", + "line.caption", "ljust", "lmar", "lty.xaxs", @@ -270,6 +275,7 @@ assign_tpar = function(opts) { assert_tpar = function(.tpar) { + assert_numeric(.tpar[["adj.caption"]], len = 1, lower = 0, upper = 1, null.ok = TRUE, name = "adj.caption") 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..7ac5b7f7 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, caption = 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(caption) + if (clines >= 1L && side == 1L) { + cex_cap = get_tpar("cex.caption", tpar_list = tpars, default = 1) + mar = mar + (cex_cap + 0.2) + (clines - 1) * cex_cap + } mar } diff --git a/man/build_legend_env.Rd b/man/build_legend_env.Rd index e4b2cccb..418a2359 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_caption = FALSE, + caption_text = NULL, new_plot = TRUE ) } diff --git a/man/draw_legend.Rd b/man/draw_legend.Rd index 971dd523..8f499a17 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_caption = FALSE, + caption_text = NULL, new_plot = TRUE, draw = TRUE, soma_target = NULL @@ -66,6 +68,10 @@ 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_caption}{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{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..82e9e5d5 100644 --- a/man/facet.Rd +++ b/man/facet.Rd @@ -49,6 +49,7 @@ draw_facet_window( has_legend, main, sub, + caption, type, xlab, x, diff --git a/man/tinyplot.Rd b/man/tinyplot.Rd index 252c8de4..a120f447 100644 --- a/man/tinyplot.Rd +++ b/man/tinyplot.Rd @@ -25,6 +25,7 @@ tinyplot(x, ...) legend = NULL, main = NULL, sub = NULL, + caption = NULL, xlab = NULL, ylab = NULL, ann = par("ann"), @@ -78,6 +79,7 @@ tinyplot(x, ...) ylim = NULL, main = NULL, sub = NULL, + caption = NULL, xlab = NULL, ylab = NULL, ann = par("ann"), @@ -292,6 +294,11 @@ legend arguments, e.g. "bty", "horiz", and so forth. \item{sub}{a subtitle for the plot.} +\item{caption}{a caption for the plot, drawn at the bottom-right. Useful for +annotations like data sources. Appearance can be customized via +\code{\link[tinyplot]{tpar}} parameters \code{adj.caption}, \code{cex.caption}, +\code{col.caption}, \code{font.caption}, and \code{line.caption}.} + \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.} From a6f79aa60ff21c8965065ec628da109cdafed27b Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Wed, 20 May 2026 11:49:17 -0700 Subject: [PATCH 2/6] switch to "cap" --- R/facet.R | 2 +- R/legend.R | 46 ++++++++++++++++++++--------------------- R/tinyplot.R | 26 +++++++++++------------ R/tinytheme.R | 20 +++++++++--------- R/title.R | 24 ++++++++++----------- R/tpar.R | 17 +++++++++------ R/utils.R | 6 +++--- man/build_legend_env.Rd | 4 ++-- man/draw_legend.Rd | 6 +++--- man/facet.Rd | 2 +- man/tinyplot.Rd | 10 ++++----- man/tpar.Rd | 5 +++++ 12 files changed, 89 insertions(+), 79 deletions(-) diff --git a/R/facet.R b/R/facet.R index 1aa8e4ff..795bbf1b 100644 --- a/R/facet.R +++ b/R/facet.R @@ -33,7 +33,7 @@ draw_facet_window = function( has_legend, main, sub, - caption, + cap, type, xlab, x, xmax, xmin, diff --git a/R/legend.R b/R/legend.R index b8e59a2e..c87f7bab 100644 --- a/R/legend.R +++ b/R/legend.R @@ -169,8 +169,8 @@ 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_caption) { - cex_cap = get_tpar("cex.caption", 1) + 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 { @@ -273,8 +273,8 @@ tinylegend = function(legend_env) { } else if (legend_env$outer_end) { if (legend_env$outer_bottom) { legend_env$ooma[1] = soma - if (legend_env$has_caption) { - cex_cap = get_tpar("cex.caption", 1) + 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 { @@ -330,17 +330,17 @@ tinylegend = function(legend_env) { do.call("legend", legend_env$args) } - if (legend_env$outer_bottom && legend_env$has_caption) { - cex_cap = get_tpar("cex.caption", 1) + if (legend_env$outer_bottom && legend_env$has_cap) { + cex_cap = get_tpar("cex.cap", 1) mtext( - legend_env$caption_text, + legend_env$cap_text, side = 1, outer = TRUE, line = par("oma")[1] - 1, cex = cex_cap, - col = get_tpar("col.caption", "black"), - adj = get_tpar(c("adj.caption", "adj")), - font = get_tpar("font.caption", 1), + col = get_tpar("col.cap", "black"), + adj = get_tpar(c("adj.cap", "adj")), + font = get_tpar("font.cap", 1), las = 1 ) } @@ -408,7 +408,7 @@ prepare_legend = function(settings) { "bubble_cex", "by", "by_continuous", - "caption", + "cap", "cex_dep", "cex_fct_adj", "col", @@ -472,7 +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_caption = text_line_count(caption) > 0L + has_cap = text_line_count(cap) > 0L # Generate labels for discrete legends if (legend_draw_flag && isFALSE(by_continuous) && (!bubble || multi_legend)) { @@ -495,7 +495,7 @@ prepare_legend = function(settings) { "legend_args", "legend_draw_flag", "has_sub", - "has_caption" + "has_cap" ) ) } @@ -737,8 +737,8 @@ build_legend_env = function( gradient, lmar, has_sub = FALSE, - has_caption = FALSE, - caption_text = NULL, + has_cap = FALSE, + cap_text = NULL, new_plot = TRUE ) { # Create legend environment @@ -748,8 +748,8 @@ build_legend_env = function( legend_env$gradient = gradient legend_env$type = type legend_env$has_sub = has_sub - legend_env$has_caption = has_caption - legend_env$caption_text = caption_text + 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 @@ -823,7 +823,7 @@ 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_caption Logical. Does the plot have a caption. Only used if +#' @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 new_plot Logical. Should we be calling plot.new internally? @@ -914,8 +914,8 @@ draw_legend = function( gradient = FALSE, lmar = NULL, has_sub = FALSE, - has_caption = FALSE, - caption_text = NULL, + has_cap = FALSE, + cap_text = NULL, new_plot = TRUE, draw = TRUE, soma_target = NULL @@ -930,7 +930,7 @@ draw_legend = function( assert_logical(gradient) assert_logical(has_sub) - assert_logical(has_caption) + assert_logical(has_cap) assert_logical(new_plot) assert_logical(draw) @@ -965,8 +965,8 @@ draw_legend = function( gradient = gradient, lmar = lmar, has_sub = has_sub, - has_caption = has_caption, - caption_text = caption_text, + has_cap = has_cap, + cap_text = cap_text, new_plot = new_plot ) diff --git a/R/tinyplot.R b/R/tinyplot.R index a2e2cd03..2768f313 100644 --- a/R/tinyplot.R +++ b/R/tinyplot.R @@ -167,10 +167,10 @@ #' 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 caption a caption for the plot, drawn at the bottom-right. Useful for +#' @param cap a caption for the plot, drawn at the bottom. Useful for #' annotations like data sources. Appearance can be customized via -#' \code{\link[tinyplot]{tpar}} parameters `adj.caption`, `cex.caption`, -#' `col.caption`, `font.caption`, and `line.caption`. +#' \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 @@ -653,7 +653,7 @@ tinyplot.default = function( legend = NULL, main = NULL, sub = NULL, - caption = NULL, + cap = NULL, xlab = NULL, ylab = NULL, ann = par("ann"), @@ -860,7 +860,7 @@ tinyplot.default = function( # misc add = add, by = by, - caption = caption, + cap = cap, dodge = NULL, dots = dots, flip = flip, @@ -1060,7 +1060,7 @@ tinyplot.default = function( .dyn = c( dynmar_side(1, xlab, main = main, sub = sub, - caption = if (.outer_sides[1]) NULL else caption, + cap = if (.outer_sides[1]) NULL else cap, side.sub = .side.sub, axis_on = !identical(xaxt, "none") && !identical(xaxt, "n"), tpars = .tpars), @@ -1146,8 +1146,8 @@ tinyplot.default = function( gradient = by_continuous, cex = lgnd_cex, has_sub = has_sub, - has_caption = has_caption, - caption_text = caption + has_cap = has_cap, + cap_text = cap ) } else { ## multi-legend case... @@ -1221,7 +1221,7 @@ tinyplot.default = function( } } - draw_title(main, sub, caption, 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) } @@ -1305,7 +1305,7 @@ tinyplot.default = function( has_legend = has_legend, main = main, sub = sub, - caption = caption, + cap = cap, type = type, xlab = xlab, x = x, xmax = xmax, xmin = xmin, @@ -1336,7 +1336,7 @@ tinyplot.default = function( has_legend = has_legend, main = main, sub = sub, - caption = caption, + cap = cap, type = type, xlab = xlab, x = datapoints$x, xmax = datapoints$xmax, xmin = datapoints$xmin, @@ -1607,7 +1607,7 @@ tinyplot.formula = function( # log = "", main = NULL, sub = NULL, - caption = NULL, + cap = NULL, xlab = NULL, ylab = NULL, ann = par("ann"), @@ -1756,7 +1756,7 @@ tinyplot.formula = function( # log = "", main = main, sub = sub, - caption = caption, + cap = cap, xlab = xlab, ylab = ylab, ann = ann, diff --git a/R/tinytheme.R b/R/tinytheme.R index d5f98691..f0d232b9 100644 --- a/R/tinytheme.R +++ b/R/tinytheme.R @@ -267,7 +267,7 @@ theme_default = list( tinytheme = "default", adj = par("adj"), # 0.5, adj.main = par("adj"), # 0.5, - adj.caption = 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", @@ -275,7 +275,7 @@ theme_default = list( cex.axis = par("cex.axis"), #1, cex.lab = par("cex.lab"), #1, cex.main = par("cex.main"), #1.2, - cex.caption = 1, + 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 @@ -285,7 +285,7 @@ theme_default = list( col.yaxs = par("col.axis"), #1, col.lab = par("col.lab"), #"black", col.main = par("col.main"), #"black", - col.caption = "black", + col.cap = "black", col.sub = par("col.sub"), #"black", dynmar = FALSE, facet.bg = NULL, @@ -296,7 +296,7 @@ theme_default = list( font.axis = par("font.axis"), # 1, font.lab = par("font.lab"), # 1, font.main = par("font.main"), # 2, - font.caption = 1, + font.cap = 1, font.sub = par("font.sub"), # 2, grid = FALSE, grid.col = "lightgray", @@ -326,7 +326,7 @@ theme_default = list( theme_basic = modifyList(theme_default, list( tinytheme = "basic", - adj.caption = 1, + adj.cap = 1, facet.bg = "gray90", facet.border = "black", grid = TRUE, @@ -335,7 +335,7 @@ theme_basic = modifyList(theme_default, list( theme_tufte = modifyList(theme_default, list( tinytheme = "tufte", - adj.caption = 1, + adj.cap = 1, adj.main = 0, adj.sub = 0, bty = "n", @@ -349,7 +349,7 @@ theme_tufte = modifyList(theme_default, list( theme_void = modifyList(theme_default, list( tinytheme = "void", - adj.caption = 1, + adj.cap = 1, adj.main = 0, adj.sub = 0, font.main = 1, @@ -398,7 +398,7 @@ theme_dynamic = modifyList(theme_basic, list( theme_clean = modifyList(theme_dynamic, list( tinytheme = "clean", - cex.caption = 0.8, + cex.cap = 0.8, grid = TRUE, palette.qualitative = "Tableau 10", palette.sequential = "ag_Sunset" @@ -407,7 +407,7 @@ theme_clean = modifyList(theme_dynamic, list( theme_classic = modifyList(theme_dynamic, list( tinytheme = "classic", bty = "l", - cex.caption = 0.8, + cex.cap = 0.8, facet.bg = NULL, font.main = 1, palette.qualitative = "Okabe-Ito" @@ -465,7 +465,7 @@ theme_dark = modifyList(theme_minimal, list( col.yaxs = "#BBBBBB", col.lab = "#BBBBBB", col.main = "#BBBBBB", - col.caption = "#888888", + col.cap = "#BBBBBB", col.sub = "#BBBBBB", col.axis = "#BBBBBB", # facet.bg = "gray20", diff --git a/R/title.R b/R/title.R index 0d00c783..2992240e 100644 --- a/R/title.R +++ b/R/title.R @@ -1,4 +1,4 @@ -draw_title = function(main, sub, caption, 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,20 +79,20 @@ draw_title = function(main, sub, caption, xlab, ylab, legend, legend_args, opar, } - caption_in_legend = !is.null(legend_args[["x"]]) && grepl("bottom!$", legend_args[["x"]]) - if (!is.null(caption) && !caption_in_legend) { - cex_cap = get_tpar("cex.caption", 1) - line_caption = get_tpar("line.caption", NULL) - if (is.null(line_caption)) { - line_caption = par("mar")[1] - 1 + 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 = caption, - line = line_caption, + text = cap, + line = line_cap, cex = cex_cap, - col = get_tpar("col.caption", "black"), - adj = get_tpar(c("adj.caption", "adj")), - font = get_tpar("font.caption", 1), + col = get_tpar("col.cap", "black"), + adj = get_tpar(c("adj.cap", "adj")), + font = get_tpar("font.cap", 1), side = 1, las = 1 ) diff --git a/R/tpar.R b/R/tpar.R index 831d2c45..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,20 +223,20 @@ get_tpar = function(opts, default = NULL, tpar_list = NULL) { known_tpar = c( - "adj.caption", + "adj.cap", "adj.main", "adj.sub", "adj.xlab", "adj.ylab", - "cex.caption", + "cex.cap", "cex.xlab", "cex.ylab", - "col.caption", + "col.cap", "col.xaxs", "col.yaxs", "cairo", "dynmar", - "font.caption", + "font.cap", "gap.axis", "gap.lab", "facet.bg", @@ -248,7 +253,7 @@ known_tpar = c( "grid.col", "grid.lty", "grid.lwd", - "line.caption", + "line.cap", "ljust", "lmar", "lty.xaxs", @@ -275,7 +280,7 @@ assign_tpar = function(opts) { assert_tpar = function(.tpar) { - assert_numeric(.tpar[["adj.caption"]], len = 1, lower = 0, upper = 1, null.ok = TRUE, name = "adj.caption") + 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 7ac5b7f7..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, caption = 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,9 +89,9 @@ dynmar_side = function(side, label, main = NULL, sub = NULL, caption = 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(caption) + clines = text_line_count(cap) if (clines >= 1L && side == 1L) { - cex_cap = get_tpar("cex.caption", tpar_list = tpars, default = 1) + 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/man/build_legend_env.Rd b/man/build_legend_env.Rd index 418a2359..3cebf653 100644 --- a/man/build_legend_env.Rd +++ b/man/build_legend_env.Rd @@ -20,8 +20,8 @@ build_legend_env( gradient, lmar, has_sub = FALSE, - has_caption = FALSE, - caption_text = NULL, + has_cap = FALSE, + cap_text = NULL, new_plot = TRUE ) } diff --git a/man/draw_legend.Rd b/man/draw_legend.Rd index 8f499a17..4bed71e2 100644 --- a/man/draw_legend.Rd +++ b/man/draw_legend.Rd @@ -20,8 +20,8 @@ draw_legend( gradient = FALSE, lmar = NULL, has_sub = FALSE, - has_caption = FALSE, - caption_text = NULL, + has_cap = FALSE, + cap_text = NULL, new_plot = TRUE, draw = TRUE, soma_target = NULL @@ -68,7 +68,7 @@ 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_caption}{Logical. Does the plot have a caption. Only used if +\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.} diff --git a/man/facet.Rd b/man/facet.Rd index 82e9e5d5..1be2b97f 100644 --- a/man/facet.Rd +++ b/man/facet.Rd @@ -49,7 +49,7 @@ draw_facet_window( has_legend, main, sub, - caption, + cap, type, xlab, x, diff --git a/man/tinyplot.Rd b/man/tinyplot.Rd index a120f447..b04c6cad 100644 --- a/man/tinyplot.Rd +++ b/man/tinyplot.Rd @@ -25,7 +25,7 @@ tinyplot(x, ...) legend = NULL, main = NULL, sub = NULL, - caption = NULL, + cap = NULL, xlab = NULL, ylab = NULL, ann = par("ann"), @@ -79,7 +79,7 @@ tinyplot(x, ...) ylim = NULL, main = NULL, sub = NULL, - caption = NULL, + cap = NULL, xlab = NULL, ylab = NULL, ann = par("ann"), @@ -294,10 +294,10 @@ legend arguments, e.g. "bty", "horiz", and so forth. \item{sub}{a subtitle for the plot.} -\item{caption}{a caption for the plot, drawn at the bottom-right. Useful for +\item{cap}{a caption for the plot, drawn at the bottom. Useful for annotations like data sources. Appearance can be customized via -\code{\link[tinyplot]{tpar}} parameters \code{adj.caption}, \code{cex.caption}, -\code{col.caption}, \code{font.caption}, and \code{line.caption}.} +\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.} 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). From 0e7edcf5afd7d3268cbf0fe980c03692ba0edf8e Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Wed, 20 May 2026 13:16:52 -0700 Subject: [PATCH 3/6] docs --- R/tinyplot.R | 15 ++++++++------- R/tinytheme.R | 3 ++- altdoc/pkgdown.yml | 2 +- man/tinyplot.Rd | 15 ++++++++------- man/tinytheme.Rd | 3 ++- vignettes/introduction.qmd | 3 ++- vignettes/themes.qmd | 21 +++++++++++++++++---- 7 files changed, 40 insertions(+), 22 deletions(-) diff --git a/R/tinyplot.R b/R/tinyplot.R index 2768f313..289d470a 100644 --- a/R/tinyplot.R +++ b/R/tinyplot.R @@ -168,9 +168,11 @@ #' @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. Appearance can be customized via -#' \code{\link[tinyplot]{tpar}} parameters `adj.cap`, `cex.cap`, -#' `col.cap`, `font.cap`, and `line.cap`. +#' 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 @@ -613,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: diff --git a/R/tinytheme.R b/R/tinytheme.R index f0d232b9..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) #' } 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/man/tinyplot.Rd b/man/tinyplot.Rd index b04c6cad..ddd49c5c 100644 --- a/man/tinyplot.Rd +++ b/man/tinyplot.Rd @@ -295,9 +295,11 @@ 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. 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}.} +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.} @@ -786,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/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 ``` From 3653bf78ff1c4cdea223ab516eb48ef00d6c62b5 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Wed, 20 May 2026 13:21:09 -0700 Subject: [PATCH 4/6] r cmd check --- R/legend.R | 2 ++ R/zzz.R | 2 ++ man/draw_legend.Rd | 3 +++ 3 files changed, 7 insertions(+) diff --git a/R/legend.R b/R/legend.R index c87f7bab..f7fa2df4 100644 --- a/R/legend.R +++ b/R/legend.R @@ -826,6 +826,8 @@ build_legend_env = function( #' @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 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/man/draw_legend.Rd b/man/draw_legend.Rd index 4bed71e2..ee2e73fe 100644 --- a/man/draw_legend.Rd +++ b/man/draw_legend.Rd @@ -72,6 +72,9 @@ margin a bit further.} 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 From 4ca3335ecacacc0c532319d49eca7abd5b6baeac Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Wed, 20 May 2026 13:30:14 -0700 Subject: [PATCH 5/6] tests --- .../_tinysnapshot/cap_bottom_legend.svg | 255 ++++++++++++++++++ inst/tinytest/_tinysnapshot/cap_clean.svg | 255 ++++++++++++++++++ .../_tinysnapshot/cap_default_theme.svg | 226 ++++++++++++++++ inst/tinytest/test-cap.R | 24 ++ 4 files changed, 760 insertions(+) create mode 100644 inst/tinytest/_tinysnapshot/cap_bottom_legend.svg create mode 100644 inst/tinytest/_tinysnapshot/cap_clean.svg create mode 100644 inst/tinytest/_tinysnapshot/cap_default_theme.svg create mode 100644 inst/tinytest/test-cap.R 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 @@ + + + + + + + + + + + + + + + + +Species +setosa +versicolor +virginica +Source: Anderson (1935) + + + + + + + +Petal.Length +Sepal.Length + + + + + + + + + + +1 +2 +3 +4 +5 +6 +7 + + + + + + + + + +4.5 +5.0 +5.5 +6.0 +6.5 +7.0 +7.5 +8.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + +Species +setosa +versicolor +virginica +Source: Anderson (1935) + + + + + + + +Petal.Length +Sepal.Length + + + + + + + + + + +1 +2 +3 +4 +5 +6 +7 + + + + + + + + + +4.5 +5.0 +5.5 +6.0 +6.5 +7.0 +7.5 +8.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + +Subtitle +`sub` and `cap` overlap for default theme +Caption +Petal.Length +Sepal.Length + + + + + + + + +1 +2 +3 +4 +5 +6 +7 + + + + + + + + + +4.5 +5.0 +5.5 +6.0 +6.5 +7.0 +7.5 +8.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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") From a69a148a8d469d5e21b2c7f8499e751d35f5a6fb Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Wed, 20 May 2026 13:46:41 -0700 Subject: [PATCH 6/6] news --- NEWS.md | 5 +++++ 1 file changed, 5 insertions(+) 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