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 @@ + + + + + + + + + + + + + + + + +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") 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 ```