diff --git a/DESCRIPTION b/DESCRIPTION index 9628fd94..009dd9cb 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: tinyplot Type: Package Title: Lightweight Extension of the Base R Graphics System -Version: 0.6.0 +Version: 0.6.99 Date: 2025-11-26 Authors@R: c( diff --git a/NEWS.md b/NEWS.md index 65807b38..0a586921 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,6 +4,21 @@ _If you are viewing this file on CRAN, please check the [latest NEWS](https://grantmcdermott.com/tinyplot/NEWS.html) on our website where the formatting is also better._ +## Development version + +### Internals + +- We now encourage type-specific legend customizations within the individual + `type_` constructors. (#531 @grantmcdermott) + +### Documentation + +- Improved guidance for + [custom types](https://grantmcdermott.com/tinyplot/vignettes/types.html#custom-types) + in the `Types` vignette. (#531 @grantmcdermott) + +### Breaking changes + ## v0.6.0 ### Breaking changes diff --git a/R/bubble.R b/R/bubble.R new file mode 100644 index 00000000..424ac5c8 --- /dev/null +++ b/R/bubble.R @@ -0,0 +1,55 @@ +bubble = function(settings) { + # Only process for points and text types + if (!(settings$type %in% c("p", "text"))) return(invisible()) + + cex = settings$cex + + # Only process if cex is a vector matching data length + if (is.null(cex) || length(cex) != nrow(settings$datapoints)) return(invisible()) + + clim = settings$clim %||% c(0.5, 2.5) + + bubble = TRUE + + ## Identify the pretty break points for our bubble labels + bubble_labs = pretty(cex, n = 5) + len_labs = length(bubble_labs) + cex = rescale_num(sqrt(c(bubble_labs, cex)) / pi, to = clim) + bubble_cex = cex[1:len_labs] + cex = cex[(len_labs+1):length(cex)] + + # catch for cases where pretty breaks leads to smallest category of 0 + if (bubble_labs[1] == 0) { + bubble_labs = bubble_labs[-1] + bubble_cex = bubble_cex[-1] + } + names(bubble_cex) = format(bubble_labs) + + if (max(clim) > 2.5) { + settings$legend_args[["x.intersp"]] = max(clim) / 2.5 + settings$legend_args[["y.intersp"]] = sapply(bubble_cex / 2.5, max, 1) + } + + ## fixme: can't assign pt.cex here b/c of dual legend gotcha (don't want to + ## override the "normal" pt.cex too) + # legend_args[["pt.cex"]] = legend_args[["pt.cex"]] %||% (settings[["cex"]] %||% par("cex")) + + # Must update settings with bubble/bubble_cex/cex before calling sanitize_bubble + env2env(environment(), settings, c("bubble", "bubble_cex", "cex")) + + sanitize_bubble(settings) +} + +sanitize_bubble = function(settings) { + env2env(settings, environment(), c("datapoints", "pch", "alpha", "bg", "cex", "bubble")) + + if (!bubble) return(invisible()) + + datapoints[["cex"]] = cex + bubble_pch = if (!is.null(pch) && length(pch)==1) pch else par("pch") + bubble_alpha = if (!is.null(alpha)) alpha else 1 + bubble_bg_alpha = if (!is.null(bg) && length(bg)==1 && is.numeric(bg) && bg > 0 && bg <=1) bg else 1 + + env2env(environment(), settings, c("datapoints", "bubble_pch", "bubble_alpha", "bubble_bg_alpha")) +} + diff --git a/R/draw_legend_utils.R b/R/draw_legend_utils.R index 46749501..fac21ae2 100644 --- a/R/draw_legend_utils.R +++ b/R/draw_legend_utils.R @@ -57,38 +57,29 @@ compute_legend_args = function( legend_args[["bty"]] = legend_args[["bty"]] %||% "n" legend_args[["horiz"]] = legend_args[["horiz"]] %||% FALSE legend_args[["xpd"]] = legend_args[["xpd"]] %||% NA - if (!isTRUE(type %in% c("p", "ribbon", "polygon", "polypath"))) { - legend_args[["lwd"]] = legend_args[["lwd"]] %||% lwd - } - if (is.null(type) || type %in% c("p", "pointrange", "errorbar", "text")) { + legend_args[["lwd"]] = legend_args[["lwd"]] %||% lwd + # special handling of pt.cex for bubble plots + # (fixme: can't handle ahead of time in bubble.R b/c of dual legend gotcha) + if (is.null(type) || type %in% c("p", "text")) { legend_args[["pt.cex"]] = legend_args[["pt.cex"]] %||% (cex %||% par("cex")) } - # turn off inner line for "barplot" type - if (identical(type, "barplot")) { - legend_args[["lty"]] = 0 - } - if (isTRUE(type %in% c("rect", "ribbon", "polygon", "polypath", "boxplot", "hist", "histogram", "spineplot", "ridge", "barplot", "violin")) || gradient) { + if (gradient) { legend_args[["pch"]] = 22 legend_args[["pt.cex"]] = legend_args[["pt.cex"]] %||% 3.5 legend_args[["y.intersp"]] = legend_args[["y.intersp"]] %||% 1.25 legend_args[["seg.len"]] = legend_args[["seg.len"]] %||% 1.25 } - if (isTRUE(type %in% c("ribbon", "hist", "histogram", "spineplot"))) { - legend_args[["pt.lwd"]] = legend_args[["pt.lwd"]] %||% 0 - } - if (identical(type, "p")) { - legend_args[["pt.lwd"]] = legend_args[["pt.lwd"]] %||% lwd - } if (identical(type, "n") && isFALSE(gradient)) { legend_args[["pch"]] = legend_args[["pch"]] %||% par("pch") } + # Special pt.bg handling for types that need color-based fills if (identical(type, "spineplot")) { legend_args[["pt.bg"]] = legend_args[["pt.bg"]] %||% legend_args[["col"]] - } - if (identical(type, "ridge") && isFALSE(gradient)) { + } else if (identical(type, "ridge") && isFALSE(gradient)) { legend_args[["pt.bg"]] = legend_args[["pt.bg"]] %||% sapply(legend_args[["col"]], function(ccol) seq_palette(ccol, n = 2)[2]) + } else { + legend_args[["pt.bg"]] = legend_args[["pt.bg"]] %||% bg } - legend_args[["pt.bg"]] = legend_args[["pt.bg"]] %||% bg legend_args[["legend"]] = legend_args[["legend"]] %||% lgnd_labs if (length(lgnd_labs) != length(eval(legend_args[["legend"]]))) { warning( diff --git a/R/tinyplot.R b/R/tinyplot.R index e7aa6bd1..1073a4c9 100644 --- a/R/tinyplot.R +++ b/R/tinyplot.R @@ -733,6 +733,9 @@ tinyplot.default = function( # type-specific settings bubble = FALSE, + bubble_pch = NULL, + bubble_alpha = NULL, + bubble_bg_alpha = NULL, ygroup = NULL, # for type_ridge() # data points and labels @@ -767,9 +770,7 @@ tinyplot.default = function( null_ylim = is.null(ylim), # when palette functions need pre-processing this check raises error null_palette = tryCatch(is.null(palette), error = function(e) FALSE), - was_area_type = identical(type, "area"), # mostly for legend - x_by = identical(x, by), # for "boxplot", "spineplot" and "ridges" - + x_by = identical(x, by), # for "boxplot", "spineplot" and "ridge" # unevaluated expressions with side effects draw = substitute(draw), @@ -884,10 +885,10 @@ tinyplot.default = function( ## bubble plot ----- # - # catch some simple aesthetics for bubble plots before the standard "by" - # grouping sanitizers (actually: will only be used for dual_legend plots but - # easiest to assign/determine now) - sanitize_bubble(settings) + # Transform cex values for bubble charts. Handles size transformation, legend + # gotchas, and aesthetic sanitization. + # Currently limited to "p" and "text" types, but could expand to others. + bubble(settings) # @@ -910,12 +911,14 @@ tinyplot.default = function( # ## aesthetics by group ----- # + by_aesthetics(settings) # ## make settings available in the environment directly ----- # + env2env(settings, environment()) @@ -983,11 +986,6 @@ tinyplot.default = function( has_sub = !is.null(sub) - if (isTRUE(was_area_type) || isTRUE(type %in% c("area", "rect", "hist", "histogram"))) { - legend_args[["pt.lwd"]] = par("lwd") - legend_args[["lty"]] = 0 - } - if (!dual_legend) { ## simple case: single legend only if (is.null(lgnd_cex)) lgnd_cex = cex * cex_fct_adj diff --git a/R/type_area.R b/R/type_area.R index c9ba59fb..13f96bfc 100644 --- a/R/type_area.R +++ b/R/type_area.R @@ -24,6 +24,14 @@ data_area = function(alpha = alpha) { # ribbon.alpha comes from parent scope, so assign it locally ribbon.alpha = ribbon.alpha + # legend customizations + settings$legend_args[["pch"]] = settings$legend_args[["pch"]] %||% 22 + settings$legend_args[["pt.cex"]] = settings$legend_args[["pt.cex"]] %||% 3.5 + settings$legend_args[["pt.lwd"]] = settings$legend_args[["pt.lwd"]] %||% par("lwd") + settings$legend_args[["lty"]] = settings$legend_args[["lty"]] %||% 0 + settings$legend_args[["y.intersp"]] = settings$legend_args[["y.intersp"]] %||% 1.25 + settings$legend_args[["seg.len"]] = settings$legend_args[["seg.len"]] %||% 1.25 + env2env(environment(), settings, c( "datapoints", "ymax", diff --git a/R/type_barplot.R b/R/type_barplot.R index 2d3dd4d4..9211ea90 100644 --- a/R/type_barplot.R +++ b/R/type_barplot.R @@ -199,6 +199,14 @@ data_barplot = function(width = 5/6, beside = FALSE, center = FALSE, FUN = NULL, xaxs = "r" xaxt = if (xaxt == "s") "l" else xaxt yaxs = "i" + + # legend customizations + settings$legend_args[["lty"]] = settings$legend_args[["lty"]] %||% 0 + settings$legend_args[["pch"]] = settings$legend_args[["pch"]] %||% 22 + settings$legend_args[["pt.cex"]] = settings$legend_args[["pt.cex"]] %||% 3.5 + settings$legend_args[["y.intersp"]] = settings$legend_args[["y.intersp"]] %||% 1.25 + settings$legend_args[["seg.len"]] = settings$legend_args[["seg.len"]] %||% 1.25 + env2env(environment(), settings, c( "datapoints", "xlab", diff --git a/R/type_boxplot.R b/R/type_boxplot.R index 898858a0..d46fe330 100644 --- a/R/type_boxplot.R +++ b/R/type_boxplot.R @@ -133,6 +133,13 @@ data_boxplot = function() { ymax = datapoints$ymax by = if (length(unique(datapoints$by)) > 1) datapoints$by else by facet = if (length(unique(datapoints$facet)) > 1) datapoints$facet else facet + + # legend customizations + settings$legend_args[["pch"]] = settings$legend_args[["pch"]] %||% 22 + settings$legend_args[["pt.cex"]] = settings$legend_args[["pt.cex"]] %||% 3.5 + settings$legend_args[["y.intersp"]] = settings$legend_args[["y.intersp"]] %||% 1.25 + settings$legend_args[["seg.len"]] = settings$legend_args[["seg.len"]] %||% 1.25 + env2env(environment(), settings, c( "x", "y", diff --git a/R/type_bubble.R b/R/type_bubble.R deleted file mode 100644 index 53e3977e..00000000 --- a/R/type_bubble.R +++ /dev/null @@ -1,15 +0,0 @@ -sanitize_bubble = function(settings) { - env2env(settings, environment(), c("datapoints", "pch", "alpha", "bg", "cex", "bubble")) - if (bubble) { - datapoints[["cex"]] = cex - bubble_pch = if (!is.null(pch) && length(pch)==1) pch else par("pch") - bubble_alpha = if (!is.null(alpha)) alpha else 1 - bubble_bg_alpha = if (!is.null(bg) && length(bg)==1 && is.numeric(bg) && bg > 0 && bg <=1) bg else 1 - } - - bubble_pch = if (bubble) bubble_pch else NULL - bubble_alpha = if (bubble) bubble_alpha else NULL - bubble_bg_alpha = if (bubble) bubble_bg_alpha else NULL - - env2env(environment(), settings, c("datapoints", "bubble_pch", "bubble_alpha", "bubble_bg_alpha")) -} diff --git a/R/type_density.R b/R/type_density.R index d4bd9b23..979b9d3a 100644 --- a/R/type_density.R +++ b/R/type_density.R @@ -149,16 +149,24 @@ data_density = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512, # flags for legend and fill dtype = if (!is.null(bg)) "ribbon" else "l" - dwas_area_type = !is.null(bg) type = dtype - was_area_type = dwas_area_type by = if (length(unique(datapoints$by)) == 1) by else datapoints$by facet = if (length(unique(datapoints$facet)) == 1) facet else datapoints$facet + + # legend customizations (only for filled density plots) + if (!is.null(bg)) { + settings$legend_args[["pch"]] = settings$legend_args[["pch"]] %||% 22 + settings$legend_args[["pt.cex"]] = settings$legend_args[["pt.cex"]] %||% 3.5 + settings$legend_args[["pt.lwd"]] = settings$legend_args[["pt.lwd"]] %||% par("lwd") + settings$legend_args[["lty"]] = settings$legend_args[["lty"]] %||% 0 + settings$legend_args[["y.intersp"]] = settings$legend_args[["y.intersp"]] %||% 1.25 + settings$legend_args[["seg.len"]] = settings$legend_args[["seg.len"]] %||% 1.25 + } + env2env(environment(), settings, c( "ylab", "type", - "was_area_type", "ribbon.alpha", "datapoints", "by", diff --git a/R/type_glm.R b/R/type_glm.R index 579e3681..63f015b5 100644 --- a/R/type_glm.R +++ b/R/type_glm.R @@ -61,6 +61,15 @@ data_glm = function(family, se, level, type, ...) { }) datapoints = do.call(rbind, dat) datapoints = datapoints[order(datapoints$facet, datapoints$by, datapoints$x), ] + + # legend customizations - same as ribbon but add line through square + settings$legend_args[["pch"]] = settings$legend_args[["pch"]] %||% 22 + settings$legend_args[["pt.cex"]] = settings$legend_args[["pt.cex"]] %||% 3.5 + settings$legend_args[["pt.lwd"]] = settings$legend_args[["pt.lwd"]] %||% 0 + settings$legend_args[["lty"]] = settings$legend_args[["lty"]] %||% par("lty") + settings$legend_args[["y.intersp"]] = settings$legend_args[["y.intersp"]] %||% 1.25 + settings$legend_args[["seg.len"]] = settings$legend_args[["seg.len"]] %||% 1.25 + env2env(environment(), settings, "datapoints") } return(fun) diff --git a/R/type_histogram.R b/R/type_histogram.R index 52adf080..5d504d22 100644 --- a/R/type_histogram.R +++ b/R/type_histogram.R @@ -145,7 +145,6 @@ data_histogram = function(breaks = "Sturges", ylab = ifelse(datapoints$freq[1], "Frequency", "Density") } - # browser() x = c(datapoints$xmin, datapoints$xmax) y = c(datapoints$ymin, datapoints$ymax) ymin = datapoints$ymin @@ -154,6 +153,15 @@ data_histogram = function(breaks = "Sturges", xmax = datapoints$xmax by = if (length(unique(datapoints$by)) == 1) by else datapoints$by facet = if (length(unique(datapoints$facet)) == 1) facet else datapoints$facet + + # legend customizations + settings$legend_args[["pch"]] = settings$legend_args[["pch"]] %||% 22 + settings$legend_args[["pt.cex"]] = settings$legend_args[["pt.cex"]] %||% 3.5 + settings$legend_args[["pt.lwd"]] = settings$legend_args[["pt.lwd"]] %||% par("lwd") + settings$legend_args[["lty"]] = settings$legend_args[["lty"]] %||% 0 + settings$legend_args[["y.intersp"]] = settings$legend_args[["y.intersp"]] %||% 1.25 + settings$legend_args[["seg.len"]] = settings$legend_args[["seg.len"]] %||% 1.25 + env2env(environment(), settings, c( "x", "y", diff --git a/R/type_lm.R b/R/type_lm.R index 9bf4a627..7a09cf75 100644 --- a/R/type_lm.R +++ b/R/type_lm.R @@ -57,6 +57,15 @@ data_lm = function(se, level, ...) { }) datapoints = do.call(rbind, dat) datapoints = datapoints[order(datapoints$facet, datapoints$by, datapoints$x), ] + + # legend customizations - same as ribbon but add line through square + settings$legend_args[["pch"]] = settings$legend_args[["pch"]] %||% 22 + settings$legend_args[["pt.cex"]] = settings$legend_args[["pt.cex"]] %||% 3.5 + settings$legend_args[["pt.lwd"]] = settings$legend_args[["pt.lwd"]] %||% 0 + settings$legend_args[["lty"]] = settings$legend_args[["lty"]] %||% par("lty") + settings$legend_args[["y.intersp"]] = settings$legend_args[["y.intersp"]] %||% 1.25 + settings$legend_args[["seg.len"]] = settings$legend_args[["seg.len"]] %||% 1.25 + env2env(environment(), settings, "datapoints") } return(fun) diff --git a/R/type_loess.R b/R/type_loess.R index eea85e6b..c0c5aeee 100644 --- a/R/type_loess.R +++ b/R/type_loess.R @@ -51,6 +51,15 @@ data_loess = function(span, degree, family, control, se, level, ...) { }) datapoints = do.call(rbind, datapoints) datapoints = datapoints[order(datapoints$facet, datapoints$by, datapoints$x), ] + + # legend customizations - same as ribbon but add line through square + settings$legend_args[["pch"]] = settings$legend_args[["pch"]] %||% 22 + settings$legend_args[["pt.cex"]] = settings$legend_args[["pt.cex"]] %||% 3.5 + settings$legend_args[["pt.lwd"]] = settings$legend_args[["pt.lwd"]] %||% 0 + settings$legend_args[["lty"]] = settings$legend_args[["lty"]] %||% par("lty") + settings$legend_args[["y.intersp"]] = settings$legend_args[["y.intersp"]] %||% 1.25 + settings$legend_args[["seg.len"]] = settings$legend_args[["seg.len"]] %||% 1.25 + env2env(environment(), settings, "datapoints") } return(fun) diff --git a/R/type_pointrange.R b/R/type_pointrange.R index 90290354..c98b61d5 100644 --- a/R/type_pointrange.R +++ b/R/type_pointrange.R @@ -49,7 +49,7 @@ draw_pointrange = function() { data_pointrange = function(dodge, fixed.dodge) { fun = function(settings, ...) { - env2env(settings, environment(), c("datapoints", "xlabs")) + env2env(settings, environment(), c("datapoints", "xlabs", "cex")) if (is.character(datapoints$x)) { datapoints$x = as.factor(datapoints$x) @@ -71,6 +71,10 @@ data_pointrange = function(dodge, fixed.dodge) { } x = datapoints$x + + # legend customizations + settings$legend_args[["pt.cex"]] = settings$legend_args[["pt.cex"]] %||% (cex %||% par("cex")) + env2env(environment(), settings, c( "x", "xlabs", diff --git a/R/type_points.R b/R/type_points.R index 781d8741..16e66663 100644 --- a/R/type_points.R +++ b/R/type_points.R @@ -43,8 +43,11 @@ type_points = function(clim = c(0.5, 2.5), dodge = 0, fixed.dodge = FALSE) { } data_points = function(clim = c(0.5, 2.5), dodge = 0, fixed.dodge = FALSE) { - fun = function(settings, cex = NULL, ...) { - env2env(settings, environment(), c("datapoints", "cex", "legend_args")) + fun = function(settings, ...) { + env2env(settings, environment(), "datapoints") + + # Store clim for bubble() function + settings$clim = clim # catch for factors (we should still be able to "force" plot these with points) if (is.factor(datapoints$x)) { @@ -69,36 +72,13 @@ data_points = function(clim = c(0.5, 2.5), dodge = 0, fixed.dodge = FALSE) { datapoints = dodge_positions(datapoints, dodge, fixed.dodge) } - bubble = FALSE - bubble_cex = 1 - if (!is.null(cex) && length(cex) == nrow(datapoints)) { - bubble = TRUE - ## Identify the pretty break points for our bubble labels - bubble_labs = pretty(cex, n = 5) - len_labs = length(bubble_labs) - cex = rescale_num(sqrt(c(bubble_labs, cex)) / pi, to = clim) - bubble_cex = cex[1:len_labs] - cex = cex[(len_labs+1):length(cex)] - # catch for cases where pretty breaks leads to smallest category of 0 - if (bubble_labs[1] == 0) { - bubble_labs = bubble_labs[-1] - bubble_cex = bubble_cex[-1] - } - names(bubble_cex) = format(bubble_labs) - if (max(clim) > 2.5) { - legend_args[["x.intersp"]] = max(clim) / 2.5 - legend_args[["y.intersp"]] = sapply(bubble_cex / 2.5, max, 1) - } - } + # legend customizations + settings$legend_args[["pt.lwd"]] = settings$legend_args[["pt.lwd"]] %||% settings$lwd env2env(environment(), settings, c( "datapoints", "xlabs", - "ylabs", - "cex", - "bubble", - "bubble_cex", - "legend_args" + "ylabs" )) } } diff --git a/R/type_polygon.R b/R/type_polygon.R index fb0f9bde..3d01814f 100644 --- a/R/type_polygon.R +++ b/R/type_polygon.R @@ -16,7 +16,7 @@ type_polygon = function(density = NULL, angle = 45) { out = list( draw = draw_polygon(density = density, angle = angle), - data = NULL, + data = data_polygon(), name = "polygon" ) class(out) = "tinyplot_type" @@ -24,6 +24,18 @@ type_polygon = function(density = NULL, angle = 45) { } +data_polygon = function() { + fun = function(settings, ...) { + # legend customizations + settings$legend_args[["pch"]] = settings$legend_args[["pch"]] %||% 22 + settings$legend_args[["pt.cex"]] = settings$legend_args[["pt.cex"]] %||% 3.5 + settings$legend_args[["y.intersp"]] = settings$legend_args[["y.intersp"]] %||% 1.25 + settings$legend_args[["seg.len"]] = settings$legend_args[["seg.len"]] %||% 1.25 + } + return(fun) +} + + draw_polygon = function(density = density, angle = 45) { fun = function(ix, iy, icol, ibg, ilty = par("lty"), ilwd = par("lwd"), ...) { polygon( diff --git a/R/type_polypath.R b/R/type_polypath.R index 29742787..dbceacff 100644 --- a/R/type_polypath.R +++ b/R/type_polypath.R @@ -21,27 +21,40 @@ #' ) #' @export type_polypath = function(rule = "winding") { - draw_polypath = function() { - fun = function(ix, iy, icol, ibg, ilty, ilwd, dots, ...) { - polypath( - x = ix, - y = iy, - border = icol, - col = ibg, - lty = ilty, - lwd = ilwd, - rule = rule - ) - } - return(fun) - } - out = list( - draw = draw_polypath(), - data = NULL, + draw = draw_polypath(rule = rule), + data = data_polypath(), name = "polypath" ) class(out) = "tinyplot_type" return(out) } + +data_polypath = function() { + fun = function(settings, ...) { + # legend customizations + settings$legend_args[["pch"]] = settings$legend_args[["pch"]] %||% 22 + settings$legend_args[["pt.cex"]] = settings$legend_args[["pt.cex"]] %||% 3.5 + settings$legend_args[["y.intersp"]] = settings$legend_args[["y.intersp"]] %||% 1.25 + settings$legend_args[["seg.len"]] = settings$legend_args[["seg.len"]] %||% 1.25 + } + return(fun) +} + + +draw_polypath = function(rule = "winding") { + fun = function(ix, iy, icol, ibg, ilty, ilwd, dots, ...) { + polypath( + x = ix, + y = iy, + border = icol, + col = ibg, + lty = ilty, + lwd = ilwd, + rule = rule + ) + } + return(fun) +} + diff --git a/R/type_rect.R b/R/type_rect.R index 173e021b..dd77cf35 100644 --- a/R/type_rect.R +++ b/R/type_rect.R @@ -27,7 +27,7 @@ type_rect = function() { out = list( draw = draw_rect(), - data = NULL, + data = data_rect(), name = "rect" ) class(out) = "tinyplot_type" @@ -35,6 +35,20 @@ type_rect = function() { } +data_rect = function() { + fun = function(settings, ...) { + # legend customizations + settings$legend_args[["pch"]] = settings$legend_args[["pch"]] %||% 22 + settings$legend_args[["pt.cex"]] = settings$legend_args[["pt.cex"]] %||% 3.5 + settings$legend_args[["pt.lwd"]] = settings$legend_args[["pt.lwd"]] %||% par("lwd") + settings$legend_args[["lty"]] = settings$legend_args[["lty"]] %||% 0 + settings$legend_args[["y.intersp"]] = settings$legend_args[["y.intersp"]] %||% 1.25 + settings$legend_args[["seg.len"]] = settings$legend_args[["seg.len"]] %||% 1.25 + } + return(fun) +} + + draw_rect = function() { fun = function(ixmin, iymin, ixmax, iymax, ilty, ilwd, icol, ibg, ...) { rect( diff --git a/R/type_ribbon.R b/R/type_ribbon.R index 2ea08028..e303a93a 100644 --- a/R/type_ribbon.R +++ b/R/type_ribbon.R @@ -154,6 +154,13 @@ data_ribbon = function(ribbon.alpha = NULL, dodge = 0, fixed.dodge = FALSE) { # ribbon.alpha comes from parent scope, so assign it locally ribbon.alpha = ribbon.alpha + # legend customizations + settings$legend_args[["pch"]] = settings$legend_args[["pch"]] %||% 22 + settings$legend_args[["pt.cex"]] = settings$legend_args[["pt.cex"]] %||% 3.5 + settings$legend_args[["pt.lwd"]] = settings$legend_args[["pt.lwd"]] %||% 0 + settings$legend_args[["y.intersp"]] = settings$legend_args[["y.intersp"]] %||% 1.25 + settings$legend_args[["seg.len"]] = settings$legend_args[["seg.len"]] %||% 1.25 + vars_to_copy = c("x", "y", "ymin", "ymax", "xlabs", "datapoints", "ribbon.alpha") if (!is.null(by)) vars_to_copy = c(vars_to_copy, "by") if (!is.null(facet)) vars_to_copy = c(vars_to_copy, "facet") diff --git a/R/type_ridge.R b/R/type_ridge.R index c8a171b1..24133f7f 100644 --- a/R/type_ridge.R +++ b/R/type_ridge.R @@ -409,6 +409,13 @@ data_ridge = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512, col = col, alpha = alpha ) + + # legend customizations + settings$legend_args[["pch"]] = settings$legend_args[["pch"]] %||% 22 + settings$legend_args[["pt.cex"]] = settings$legend_args[["pt.cex"]] %||% 3.5 + settings$legend_args[["y.intersp"]] = settings$legend_args[["y.intersp"]] %||% 1.25 + settings$legend_args[["seg.len"]] = settings$legend_args[["seg.len"]] %||% 1.25 + env2env(environment(), settings, c( "datapoints", "yaxt", diff --git a/R/type_spineplot.R b/R/type_spineplot.R index 7edcb192..36ea2d78 100644 --- a/R/type_spineplot.R +++ b/R/type_spineplot.R @@ -279,6 +279,14 @@ data_spineplot = function(off = NULL, breaks = NULL, xlevels = xlevels, ylevels x_by = x_by, y_by = y_by ) + + # legend customizations + settings$legend_args[["pch"]] = settings$legend_args[["pch"]] %||% 22 + settings$legend_args[["pt.cex"]] = settings$legend_args[["pt.cex"]] %||% 3.5 + settings$legend_args[["pt.lwd"]] = settings$legend_args[["pt.lwd"]] %||% 0 + settings$legend_args[["y.intersp"]] = settings$legend_args[["y.intersp"]] %||% 1.25 + settings$legend_args[["seg.len"]] = settings$legend_args[["seg.len"]] %||% 1.25 + env2env(environment(), settings, c( "x", "y", "ymin", "ymax", "xmin", "xmax", "col", "bg", "datapoints", "by", "facet", "axes", "frame.plot", "xaxt", "yaxt", "xaxs", "yaxs", diff --git a/R/type_text.R b/R/type_text.R index 425bf81e..f0774be9 100644 --- a/R/type_text.R +++ b/R/type_text.R @@ -85,8 +85,12 @@ type_text = function( } data_text = function(labels = NULL, clim = c(0.5, 2.5)) { - fun = function(settings, cex = NULL, ...) { + fun = function(settings, ...) { env2env(settings, environment(), "datapoints") + + # Store clim for bubble() function + settings$clim = clim + if (is.null(labels)) { labels = datapoints$y } @@ -102,35 +106,7 @@ data_text = function(labels = NULL, clim = c(0.5, 2.5)) { datapoints$y = as.numeric(datapoints$y) } - bubble = FALSE - bubble_cex = 1 - if (!is.null(cex) && length(cex) == nrow(datapoints)) { - bubble = TRUE - ## Identify the pretty break points for our bubble labels - bubble_labs = pretty(cex, n = 5) - len_labs = length(bubble_labs) - # cex = rescale_num(c(bubble_labs, cex), to = clim) - cex = rescale_num(sqrt(c(bubble_labs, cex)) / pi, to = clim) - bubble_cex = cex[1:len_labs] - cex = cex[(len_labs + 1):length(cex)] - # catch for cases where pretty breaks leads to smallest category of 0 - if (bubble_labs[1] == 0) { - bubble_labs = bubble_labs[-1] - bubble_cex = bubble_cex[-1] - } - names(bubble_cex) = format(bubble_labs) - if (max(clim) > 2.5) { - legend_args[["x.intersp"]] = max(clim) / 2.5 - legend_args[["y.intersp"]] = sapply(bubble_cex / 2.5, max, 1) - } - } - - env2env(environment(), settings, c( - "datapoints", - "cex", - "bubble", - "bubble_cex" - )) + env2env(environment(), settings, "datapoints") } return(fun) } diff --git a/R/type_violin.R b/R/type_violin.R index d9e3db2a..f1d7969d 100644 --- a/R/type_violin.R +++ b/R/type_violin.R @@ -207,6 +207,13 @@ data_violin = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512, by = if (length(unique(datapoints$by)) == 1) by else datapoints$by facet = if (length(unique(datapoints$facet)) == 1) facet else datapoints$facet + + # legend customizations + settings$legend_args[["pch"]] = settings$legend_args[["pch"]] %||% 22 + settings$legend_args[["pt.cex"]] = settings$legend_args[["pt.cex"]] %||% 3.5 + settings$legend_args[["y.intersp"]] = settings$legend_args[["y.intersp"]] %||% 1.25 + settings$legend_args[["seg.len"]] = settings$legend_args[["seg.len"]] %||% 1.25 + env2env(environment(), settings, c( "datapoints", "by", diff --git a/inst/tinytest/_tinysnapshot/custom_type_log.svg b/inst/tinytest/_tinysnapshot/custom_type_log.svg new file mode 100644 index 00000000..af40ee85 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/custom_type_log.svg @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + +factor(am) +0 +1 + + + + + + + +Custom: type_log() +wt +mpg + + + + + + + + + + +0.4 +0.6 +0.8 +1.0 +1.2 +1.4 +1.6 + + + + + + + +2.4 +2.6 +2.8 +3.0 +3.2 +3.4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/test-type_custom.R b/inst/tinytest/test-type_custom.R new file mode 100644 index 00000000..92f00868 --- /dev/null +++ b/inst/tinytest/test-type_custom.R @@ -0,0 +1,34 @@ +source("helpers.R") +using("tinysnapshot") + +# type_log +f = function () { + + # based on the "redux" example in the types vignette + type_log = function(base = exp(1)) { + data_log = function() { + fun = function(settings, ...) { + datapoints = settings$datapoints + datapoints$x = log(datapoints$x, base = base) + datapoints$y = log(datapoints$y, base = base) + datapoints = datapoints[order(datapoints$x), ] + settings$datapoints = datapoints + settings$type = "p" + } + return(fun) + } + out = list( + data = data_log(), + draw = NULL, + name = "log" + ) + class(out) = "tinyplot_type" + return(out) + } + + tinyplot(mpg ~ wt | factor(am), data = mtcars, + pch = "by", + type = type_log(), + main = "Custom: type_log()") +} +expect_snapshot_plot(f, label = "custom_type_log") \ No newline at end of file diff --git a/vignettes/types.qmd b/vignettes/types.qmd index a248e9c1..f5962bcf 100644 --- a/vignettes/types.qmd +++ b/vignettes/types.qmd @@ -166,59 +166,35 @@ tinyplot(Temp ~ Wind | Month, data = aq, facet = "by", type = "lm") It is easy to add custom types to **tinyplot**. Users who need highly customized plots, or developers who want to add support to their package or functions, only -need to define three simple functions: `data_()`, `draw_()`, and -`type_()`. - -In this section, we explain the role of each of these functions and present a -minimalist example of a custom type. Interested readers may refer to -the **tinyplot** [source code](https://github.com/grantmcdermott/tinyplot) -to see many more examples, since each **tinyplot** type is itself implemented as a -custom type. - -The three functions that we need to define for a new type are: - -1. `data_()`: Data function factory. - - Must take two arguments: - - `settings` is a special internal environment containing useful parameters that can be read or modified by `tinyplot` functions, including `datapoints`, `by`, `facet`, `xlab`, `ylab`, `xlim`, `ylim`, `palette`, and many more (see below) - - `...` - - Defines the data that will be passed on to the plotting functions (including any necessary transformations). Note that this function should modify the `settings` environment in-place; it does not need to return anything -2. `draw_()`: Drawing function factory. - - Accepts information about data point values and aesthetics. - - Inputs must include `...` - - The `i` prefix in argument names indicates that we are operating on a subgroup of the data, identified by `facet` or using the `|` operator in a formula. - - Available arguments are: `ibg`, `icol`, `ilty`, `ilwd`, `ipch`, `ix`, `ixmax`, `ixmin`, `iy`, `iymax`, `iymin`, `cex`, `dots`, `type`, `x_by`, `i`, `facet_by`, `by_data`, `facet_data`, `flip` - - Returns a function which can call base R to draw the plot. -3. `type_()`: A wrapper function that returns a named list with three elements: - - `draw` - - `data` - - `name` +need to define their own `type_()` function. Let's start with a simple +example to see how this works in practice. ### A minimal example -Here is a minimal example of a custom type that logs both `x` and `y` and -plots points. Note how we use `env2env()` to extract variables from the `settings` -environment, modify them, and copy the modified values back: +Here is a minimal example of a custom `type_log()` that logs both `x` and `y` +before plotting points. ```{r} -#| layout-ncol: 2 type_log = function(base = exp(1)) { + # data tranformation function data_log = function() { fun = function(settings, ...) { - # Extract datapoints from settings environment + # extract raw datapoints from settings environment datapoints = settings$datapoints - # Transform the data + # transform (log) the data datapoints$x = log(datapoints$x, base = base) datapoints$y = log(datapoints$y, base = base) datapoints = datapoints[order(datapoints$x), ] - # Assign (inject) modified datapoints back to settings + # re-assign modified datapoints back to settings settings$datapoints = datapoints } return(fun) } + # drawing function draw_log = function() { fun = function(ix, iy, icol, ...) { points( @@ -230,45 +206,165 @@ type_log = function(base = exp(1)) { return(fun) } + # return object (list) out = list( + data = data_log(), draw = draw_log(), + name = "p" # fallback behaviour same as "p" type (e.g., legend defaults) + ) + class(out) = "tinyplot_type" + return(out) + +} +``` + +To use, simply pass to `type = type_log()` as per as normal. + +```{r} +#| layout-ncol: 2 +tinyplot(mpg ~ wt | factor(am), data = mtcars, + type = type_log(), main = "Ln (natural logarithm)") + +tinyplot(mpg ~ wt | factor(am), data = mtcars, + type = type_log(base = 10), main = "Log (base 10)") +``` + +### How it works + +As illustrated by our minimal example, custom **tinyplot** types require three +key functions: + +1. **`type_()`**: The main wrapper function that users will call. It + should accept any type-specific arguments that users will need to adjust the + final plot (e.g., here: `base`). More importantly, it _must_ return a list + of class `"tinyplot_type"` and contain three elements: + - `data`: A data transformation function (see below) + - `draw`: A drawing function (see below) + - `name`: The name of the underlying plot type (this can be anything, but + think of it as a convenient way to inherit default/fallback behaviour from + an existing plot type) + +2. **`data_()`**: A function factory that returns a data transformation + function: + - **Required arguments**: `settings` and `...` + - `settings` is an environment containing plot data and parameters, + including `datapoints` (a data frame with `x`, `y`, `by`, etc.), + `legend_args` (a list of legend customizations), and many other internal + parameters (see [Appendix: Available settings](#appendix-available-settings) + below for a complete list). + - Should modify `settings` in-place (e.g., `settings$datapoints = ...`) + - Does not need to return anything + +3. **`draw_()`**: A function factory that returns a drawing function: + - **Required arguments**: `...` + - **Available arguments**: `ix`, `iy`, `ixmin`, `ixmax`, `iymin`, `iymax`, + `icol`, `ibg`, `ipch`, `ilty`, `ilwd`, `icex`, and others + - The `i` prefix indicates data for a single group (when using `|` grouping + or facets) + - Should call base R graphics functions (e.g., `points()`, `lines()`, + `polygon()`) to render the plot + +The key insight is that `data_*()` transforms the data once, while `draw_*()` +is called repeatedly for each group or facet. + +### Pro tip: Re-use existing plot types + +Rather than code up all of the internals for a custom type from scratch, users +are strongly encouraged to re-use the scaffolding for existing types. One of the +easiest ways to do this---which we use repeatedly in the main **tinyplot** +codebase---is by "delegating" to an existing type at the end of the +`data_` function. Again, this is perhaps easier seen than explained, so +consider an alternate version of our previous `type_log()` function. + +```{r} +type_log_redux = function(base = exp(1)) { + + # data tranformation function + data_log = function() { + fun = function(settings, ...) { + datapoints = settings$datapoints + + datapoints$x = log(datapoints$x, base = base) + datapoints$y = log(datapoints$y, base = base) + datapoints = datapoints[order(datapoints$x), ] + + settings$datapoints = datapoints + + # re-assign/delegate to "p" type + settings$type = "p" + } + return(fun) + } + + # return object (list) + out = list( data = data_log(), + draw = NULL, # NULL => fall back to default type (now "p") drawing function name = "log" ) class(out) = "tinyplot_type" return(out) + } +``` + +The key differences from our original `type_log` function are: + +- `settings$type = "p"` (re-assign the type at the end of `data_log()`) +- `draw = NULL` (drop the dedicated `draw_log()` function entirely; will fall back to `draw_points()` internally now) +This "redux" version is obviously shorter than our original version, since we +can skip the custom `draw_log()` section. But re-using existing **tinyplot** +scaffolding brings added benefits beyond code concision. In particular, it +guarantees that all of the functionality from an existing `type` will be made +available to your custom type. For example, in our first `type_log()` function +we didn't account for the fact that users might want to adjust groups by other +aesthetics (e.g, `pch`), whereas we get this for free in `type_log_redux()`. + +```{r} +#| layout-ncol: 2 tinyplot(mpg ~ wt | factor(am), data = mtcars, - type = type_log(), main = "Ln") + pch = "by", + type = type_log(), main = "type_log") tinyplot(mpg ~ wt | factor(am), data = mtcars, - type = type_log(base = 10), main = "Log 10") + pch = "by", + type = type_log_redux(), main = "type_log_redux") ``` +Moral of the story: be lazy and re-use existing **tinyplot** scaffolding +wherever possible; especially drawing functions. + ### More examples The **tinyplot** [source code](https://github.com/grantmcdermott/tinyplot/tree/main/R) contains many examples of type constructor functions that should provide a -helpful starting point for custom plot types. Failing that, the **tinyplot** -team are always happy to help guide users on how to create their own types and -re-purpose existing **tinyplot** code. Just let us know by +helpful starting point for custom plot types. Each built-in **tinyplot** type +is itself implemented as a custom type, so you can see real-world examples of +varying complexity. For example, take a look at the +[`type_points.R`](https://github.com/grantmcdermott/tinyplot/blob/main/R/type_points.R) or +[`type_area.R`](https://github.com/grantmcdermott/tinyplot/blob/main/R/type_area.R) +code. (The latter provides an example where we delegate to the `type_ribbon` +drawing code.) + +Beyond that, the **tinyplot** team are always happy to help guide users on how +to create their own types. Just let us know by [raising an issue](https://github.com/grantmcdermott/tinyplot/issues) on our GitHub repo. +## Appendix: Available settings {#appendix-available-settings} -### Available settings - -To see what parameters are available in the `settings` environment, we can create -a simple `type_error()` that stops with the names of all available settings: +To see what objects and parameters are available in the `settings` environment, +we can create a simple `type_error()` function that stops with the names of all +available settings: ```{r} #| error: true type_error = function() { data_error = function() { fun = function(settings, ...) { - stop(paste(names(settings), collapse = ", ")) + stop(paste(sort(names(settings)), collapse = ", ")) } return(fun) }