diff --git a/NAMESPACE b/NAMESPACE index 06fb9663..225fc632 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -9,6 +9,7 @@ export(plt) export(plt_add) export(tinyplot) export(tinyplot_add) +export(tinytheme) export(tpar) export(type_abline) export(type_area) @@ -37,6 +38,7 @@ export(type_spline) export(type_vline) importFrom(grDevices,adjustcolor) importFrom(grDevices,as.raster) +importFrom(grDevices,axisTicks) importFrom(grDevices,col2rgb) importFrom(grDevices,colorRampPalette) importFrom(grDevices,convertColor) diff --git a/R/by_aesthetics.R b/R/by_aesthetics.R index d94bb139..7a93a9bb 100755 --- a/R/by_aesthetics.R +++ b/R/by_aesthetics.R @@ -6,6 +6,15 @@ by_col = function(ngrps = 1L, col = NULL, palette = NULL, gradient = NULL, order ngrps = 100L } + if (is.null(palette)) { + pal_qual = get_tpar("palette.qualitative", default = NULL) + if (ngrps <= max(c(length(pal_qual), 8))) { + palette = pal_qual + } else { + palette = get_tpar("palette.sequential", default = NULL) + } + } + # palette = substitute(palette, env = parent.env(environment())) # special "by" convenience keyword (will treat as NULL & handle grouping below) diff --git a/R/draw_legend.R b/R/draw_legend.R index c6032850..9a154c17 100644 --- a/R/draw_legend.R +++ b/R/draw_legend.R @@ -208,7 +208,8 @@ draw_legend = function( ## restore inner margin defaults ## (in case the plot region/margins were affected by the preceding tinyplot call) - if (any(ooma != 0)) { + dynmar = isTRUE(.tpar[["dynmar"]]) + if (any(ooma != 0) && !dynmar) { if ( ooma[1] != 0 & omar[1] == par("mgp")[1] + 1*par("cex.lab") ) omar[1] = 5.1 if ( ooma[2] != 0 & omar[2] == par("mgp")[1] + 1*par("cex.lab") ) omar[2] = 4.1 if ( ooma[3] == topmar_epsilon & omar[3] != 4.1 ) omar[3] = 4.1 @@ -219,7 +220,6 @@ draw_legend = function( par(omd = c(0,1,0,1)) ooma = par("oma") - ## Legend to outer side (either right or left) of plot if (grepl("right!$|left!$", legend_args[["x"]])) { @@ -244,7 +244,22 @@ draw_legend = function( } par(mar = omar) - if (isTRUE(new_plot)) plot.new() + # if (isTRUE(new_plot)) plot.new() + if (isTRUE(new_plot)) { + plot.new() + # Experimental: For themed + dynamic plots, we need to make sure the + # adjusted plot margins for the legend are reinstated (after being + # overwritten by the before.plot.new hook. + if (dynmar) { + omar = par("mar") + if (outer_right) { + omar[4] = 0 + } else { + omar[2] = par("mgp")[1] + 1*par("cex.lab") + } + par(mar = omar) + } + } legend_args[["horiz"]] = FALSE @@ -290,9 +305,13 @@ draw_legend = function( # GM: The legend inset spacing only works _exactly_ if we refresh the plot # area. I'm not sure why (and it works properly if we use the same # parameters manually while debugging), but this hack seems to work. - par(new = TRUE) + ## v0.3.0 update: Using (temporary) hook instead of direct par(new = TRUE) + ## assignment to play nice with tinytheme logic. + oldhook = getHook("before.plot.new") + setHook("before.plot.new", function() par(new = TRUE), action = "append") + setHook("before.plot.new", function() par(mar = omar), action = "append") plot.new() - par(new = FALSE) + setHook("before.plot.new", oldhook, action = "replace") # Finally, set the inset as part of the legend args. legend_args[["inset"]] = c(1+inset, 0) @@ -310,7 +329,7 @@ draw_legend = function( ## width---will be off the first time. if (outer_bottom) { omar[1] = par("mgp")[1] + 1*par("cex.lab") - if (isTRUE(has_sub)) omar[1] = omar[1] + 1*par("cex.sub") + if (isTRUE(has_sub) && (is.null(.tpar[["side.sub"]]) || .tpar[["side.sub"]]==1)) omar[1] = omar[1] + 1*par("cex.sub") } else { ## For "top!", the logic is slightly different: We don't expand the outer ## margin b/c we need the legend to come underneath the main title. So @@ -320,7 +339,25 @@ draw_legend = function( } par(mar = omar) - if (isTRUE(new_plot)) plot.new() + # if (isTRUE(new_plot)) plot.new() + if (isTRUE(new_plot)) { + plot.new() + # Experimental: For themed + dynamic plots, we need to make sure the + # adjusted plot margins for the legend are reinstated (after being + # overwritten by the before.plot.new hook. + if (dynmar) { + omar = par("mar") + if (outer_bottom) { + # omar[1] = par("mgp")[1] + 1*par("cex.lab") + omar[1] = theme_clean$mgp[1] + 1*par("cex.lab") ## bit of a hack + if (isTRUE(has_sub) && (is.null(.tpar[["side.sub"]]) || .tpar[["side.sub"]]==1)) omar[1] = omar[1] + 1*par("cex.sub") + } else { + ooma[3] = ooma[3] + topmar_epsilon + par(oma = ooma) + } + par(mar = omar) + } + } legend_args[["horiz"]] = TRUE @@ -381,9 +418,13 @@ draw_legend = function( # GM: The legend inset spacing only works _exactly_ if we refresh the plot # area. I'm not sure why (and it works properly if we use the same # parameters manually while debugging), but this hack seems to work. - par(new = TRUE) + ## v0.3.0 update: Using (temporary) hook instead of direct par(new = TRUE) + ## assignment to play nice with tinytheme logic. + oldhook = getHook("before.plot.new") + setHook("before.plot.new", function() par(new = TRUE), action = "append") + setHook("before.plot.new", function() par(mar = omar), action = "append") ## experimental dynmar plot.new() - par(new = FALSE) + setHook("before.plot.new", oldhook, action = "replace") # Finally, set the inset as part of the legend args. legend_args[["inset"]] = c(0, 1+inset) diff --git a/R/facet.R b/R/facet.R index a5f74b27..d8056a01 100644 --- a/R/facet.R +++ b/R/facet.R @@ -1,6 +1,6 @@ # Facet layout structure # -# This function is called by `tinyplot`. Given some inputs, it returns +# This function is called by `tinyplot`. Given some inputs, it returns # information about the layout of the facets. # facet_layout = function(facet, add = FALSE, facet.args = list()) { @@ -49,7 +49,7 @@ facet_layout = function(facet, add = FALSE, facet.args = list()) { facets = ifacet = nfacets = oxaxis = oyaxis = 1 cex_fct_adj = 1 } - + list( facets = facets, ifacet = ifacet, @@ -119,11 +119,19 @@ get_facet_fml = function(formula, data = NULL) { # internal function to draw window with different facets, grids, axes, etc. draw_facet_window = function(grid, ...) { - list2env(list(...), environment()) - if (isFALSE(add)) { + # draw background color only in the grid rectangle + grid.bg = get_tpar("grid.bg") + if (!is.null(grid.bg)) { + corners = par("usr") + rect(corners[1], corners[3], corners[2], corners[4], col = grid.bg, border = NA) + } + ## dynamic margins flag + dynmar = isTRUE(.tpar[["dynmar"]]) + + if (isFALSE(add)) { ## optionally allow to modify the style of axis interval calculation if (!is.null(xaxs)) par(xaxs = xaxs) if (!is.null(yaxs)) par(yaxs = yaxs) @@ -168,6 +176,41 @@ draw_facet_window = function(grid, ...) { fmar[3] = fmar[3] + facet_newlines * facet_text / cex_fct_adj omar = par("mar") + + ## Dynamic plot margin adjustments + if (dynmar) { + if (par("las") %in% 1:2) { + # extra whitespace bump on the y axis + # yaxl = axTicks(2) + yaxl = axisTicks(usr = extendrange(ylim, f = 0.04), log = par("ylog")) + # whtsbp = grconvertX(max(strwidth(yaxl, "figure")), from = "nfc", to = "lines") - 1 + whtsbp = grconvertX(max(strwidth(yaxl, "figure")), from = "nfc", to = "lines") - grconvertX(0, from = "nfc", to = "lines") - 1 + if (whtsbp > 0) { + omar = omar + c(0, whtsbp, 0, 0) * cex_fct_adj + fmar[2] = fmar[2] + whtsbp * cex_fct_adj + } + } + if (par("las") %in% 2:3) { + # extra whitespace bump on the x axis + # xaxl = axTicks(1) + xaxl = axisTicks(usr = extendrange(xlim, f = 0.04), log = par("xlog")) + whtsbp = grconvertY(max(strwidth(xaxl, "figure")), from = "nfc", to = "lines") - 1 + # whtsbp = grconvertY(max(strwidth(xaxl, "figure")), from = "nfc", to = "lines") - grconvertY(0, from = "nfc", to = "lines") - 1 + if (whtsbp > 0) { + omar = omar + c(whtsbp, 0, 0, 0) * cex_fct_adj + fmar[1] = fmar[1] + whtsbp * cex_fct_adj + } + } + # FIXME: Is this causing issues for lhs legends with facet_grid? + # catch for missing rhs legend + if (isTRUE(attr(facet, "facet_grid")) && !has_legend) { + omar[4] = omar[4] + 1 + } + # Extra reduction if no plot frame to reduce whitespace + if (isFALSE(frame.plot) && !isTRUE(facet.args[["free"]])) { + fmar[2] = fmar[2] - (whtsbp * cex_fct_adj) + } + } # Now we set the margins. The trick here is that we simultaneously adjust # inner (mar) and outer (oma) margins by the same amount, but in opposite @@ -189,6 +232,31 @@ draw_facet_window = function(grid, ...) { # Now that the margins have been set, arrange facet rows and columns based # on our earlier calculations. par(mfrow = c(nfacet_rows, nfacet_cols)) + } else if (dynmar) { + # Dynamic plot margin adjustments + omar = par("mar") + omar = omar - c(0, 0, 1, 0) # reduce top whitespace since no facet (title) + if (par("las") %in% 1:2) { + # extra whitespace bump on the y axis + # yaxl = axTicks(2) + yaxl = axisTicks(usr = extendrange(ylim, f = 0.04), log = par("ylog")) + # whtsbp = grconvertX(max(strwidth(yaxl, "figure")), from = "nfc", to = "lines") - 1 + whtsbp = grconvertX(max(strwidth(yaxl, "figure")), from = "nfc", to = "lines") - grconvertX(0, from = "nfc", to = "lines") - 1 + if (whtsbp > 0) { + omar[2] = omar[2] + whtsbp + } + } + if (par("las") %in% 2:3) { + # extra whitespace bump on the x axis + # xaxl = axTicks(1) + xaxl = axisTicks(usr = extendrange(ylim, f = 0.04), log = par("xlog")) + whtsbp = grconvertY(max(strwidth(xaxl, "figure")), from = "nfc", to = "lines") - 1 + # whtsbp = grconvertY(max(strwidth(xaxl, "figure")), from = "nfc", to = "lines") - grconvertY(0, from = "nfc", to = "lines") - 1 + if (whtsbp > 0) { + omar[1] = omar[1] + whtsbp + } + } + par(mar = omar) } ## Loop over the individual facet windows and draw the plot region @@ -236,10 +304,32 @@ draw_facet_window = function(grid, ...) { yside = 2 } - + # axes, frame.plot and grid if (isTRUE(axes) || isTRUE(facet.args[["free"]])) { - + args_x = list(x, + side = xside, + type = xaxt, + cex = get_tpar(c("cex.xaxs", "cex.axis"), 0.8), + lwd = get_tpar(c("lwd.xaxs", "lwd.axis"), 1), + lty = get_tpar(c("lty.xaxs", "lty.axis"), 1) + ) + args_y = list(y, + side = yside, + type = yaxt, + cex = get_tpar(c("cex.yaxs", "cex.axis"), 0.8), + lwd = get_tpar(c("lwd.yaxs", "lwd.axis"), 1), + lty = get_tpar(c("lty.yaxs", "lty.axis"), 1) + ) + type_range_x = type %in% c("pointrange", "errorbar", "ribbon", "boxplot", "p") && !is.null(xlabs) + type_range_y = isTRUE(flip) && type %in% c("pointrange", "errorbar", "ribbon", "boxplot", "p") && !is.null(ylabs) + if (type_range_x) { + args_x = modifyList(args_x, list(at = xlabs, labels = names(xlabs))) + } + if (type_range_y) { + args_y = modifyList(args_y, list(at = ylabs, labels = names(ylabs))) + } + if (isTRUE(facet.args[["free"]]) && (par("xlog") || par("ylog"))) { warning( "\nFree scale axes for faceted plots are currently not supported if the axes are logged. Reverting back to fixed scales.", @@ -248,20 +338,20 @@ draw_facet_window = function(grid, ...) { ) facet.args[["free"]] = FALSE } - + # Special logic if facets are free... if (isTRUE(facet.args[["free"]])) { # First, we need to calculate the plot extent and axes range of each # individual facet. xfree = split(c(x, xmin, xmax), facet)[[ii]] yfree = split(c(y, ymin, ymax), facet)[[ii]] - xlim = range(xfree, na.rm = TRUE) + xlim = range(xfree, na.rm = TRUE) ylim = range(yfree, na.rm = TRUE) xext = extendrange(xlim, f = 0.04) yext = extendrange(ylim, f = 0.04) # We'll save this in a special .fusr env var (list) that we'll re-use # when it comes to plotting the actual elements later - if (ii==1) { + if (ii == 1) { fusr = replicate(4, vector("double", length = nfacets), simplify = FALSE) assign(".fusr", fusr, envir = get(".tinyplot_env", envir = parent.env(environment()))) } @@ -281,38 +371,16 @@ draw_facet_window = function(grid, ...) { } else { tinyAxis(yfree, side = yside, type = yaxt) } - - # For fixed facets we can just reuse the same plot extent and axes limits + + # For fixed facets we can just reuse the same plot extent and axes limits } else if (isTRUE(frame.plot)) { # if plot frame is true then print axes per normal... - if (type %in% c("pointrange", "errorbar", "ribbon", "boxplot", "p") && !is.null(xlabs)) { - tinyAxis(x, side = xside, at = xlabs, labels = names(xlabs), type = xaxt) - } else { - tinyAxis(x, side = xside, type = xaxt) - } - # tinyAxis(y, side = yside, type = yaxt) - if (isTRUE(flip) && type %in% c("pointrange", "errorbar", "ribbon", "boxplot", "p") && !is.null(ylabs)) { - tinyAxis(y, side = yside, at = ylabs, labels = names(ylabs), type = yaxt) - } else { - tinyAxis(y, side = yside, type = yaxt) - } + do.call(tinyAxis, args_x) + do.call(tinyAxis, args_y) } else { # ... else only print the "outside" axes. - if (ii %in% oxaxis) { - if (type %in% c("pointrange", "errorbar", "ribbon", "boxplot", "p") && !is.null(xlabs)) { - tinyAxis(x, side = xside, at = xlabs, labels = names(xlabs), type = xaxt) - } else { - tinyAxis(x, side = xside, type = xaxt) - } - } - if (ii %in% oyaxis) { - # tinyAxis(y, side = yside, type = yaxt) - if (isTRUE(flip) && type %in% c("pointrange", "errorbar", "ribbon", "boxplot", "p") && !is.null(ylabs)) { - tinyAxis(y, side = yside, at = ylabs, labels = names(ylabs), type = yaxt) - } else { - tinyAxis(y, side = yside, type = yaxt) - } - } + if (ii %in% oxaxis) do.call(tinyAxis, args_x) + if (ii %in% oyaxis) do.call(tinyAxis, args_y) } } @@ -380,7 +448,6 @@ draw_facet_window = function(grid, ...) { if (xlog) { line_height = grconvertX(line_height, from = "lines", to = "user") / grconvertX(0, from = "lines", to = "user") rect_width = corners[2] * line_height - } else { line_height = grconvertX(line_height, from = "lines", to = "user") - grconvertX(0, from = "lines", to = "user") rect_width = corners[2] + line_height @@ -394,7 +461,6 @@ draw_facet_window = function(grid, ...) { if (xlog) { xpos = grconvertX(0.4, from = "lines", to = "user") / grconvertX(0, from = "lines", to = "user") xpos = corners[2] * xpos - } else { xpos = grconvertX(0.4, from = "lines", to = "user") - grconvertX(0, from = "lines", to = "user") xpos = corners[2] + xpos @@ -498,10 +564,9 @@ draw_facet_window = function(grid, ...) { grid } } - + # drawn elements if (!is.null(draw)) eval(draw) - } # end of ii facet loop } # end of add check @@ -519,5 +584,6 @@ is_facet_position = function(position, ifacet, facet_window_args) { "right" = ifacet %in% pmin(ni, seq(1L, ni, by = nc) + nc - 1L), "top" = ifacet %in% head(id, nc), "bottom" = ifacet %in% tail(id, nc), - NA) + NA + ) } diff --git a/R/get_saved_par.R b/R/get_saved_par.R index ede49387..6998eee6 100644 --- a/R/get_saved_par.R +++ b/R/get_saved_par.R @@ -102,14 +102,14 @@ #' tpar(sp) #' #' @export -get_saved_par = function(when = c("before", "after")) { +get_saved_par = function(when = c("before", "after", "first")) { when = match.arg(when) par_env_name = paste0(".saved_par_", when) return(get(par_env_name, envir = get(".tinyplot_env", envir = parent.env(environment())))) } # (non-exported) companion function(s) for setting the original pars -set_saved_par = function(when = c("before", "after"), value) { +set_saved_par = function(when = c("before", "after", "first"), value) { when = match.arg(when) par_env_name = paste0(".saved_par_", when) assign(par_env_name, value, envir = get(".tinyplot_env", envir = parent.env(environment()))) diff --git a/R/tinyAxis.R b/R/tinyAxis.R index 8aaf3c66..d7a402a9 100644 --- a/R/tinyAxis.R +++ b/R/tinyAxis.R @@ -1,4 +1,6 @@ -## auxiliary Axis() interface with different parameter combinations based on type +#' auxiliary Axis() interface with different parameter combinations based on type +#' +#' @keywords internal tinyAxis = function(x = NULL, ..., type = "standard") { type = match.arg(type, c("standard", "none", "labels", "ticks", "axis")) if (type == "none") { diff --git a/R/tinyplot.R b/R/tinyplot.R index 64ce8b85..72f8f6af 100644 --- a/R/tinyplot.R +++ b/R/tinyplot.R @@ -314,7 +314,7 @@ #' out existing `plot` calls for `tinyplot` (or its shorthand alias `plt`), #' without causing unexpected changes to the output. #' -#' @importFrom grDevices adjustcolor colorRampPalette extendrange palette palette.colors palette.pals hcl.colors hcl.pals xy.coords png jpeg pdf svg dev.off dev.new dev.list +#' @importFrom grDevices axisTicks adjustcolor colorRampPalette extendrange palette palette.colors palette.pals hcl.colors hcl.pals xy.coords png jpeg pdf svg dev.off dev.new dev.list #' @importFrom graphics abline arrows axis Axis box boxplot grconvertX grconvertY hist lines mtext par plot.default plot.new plot.window points polygon polypath segments rect text title #' @importFrom utils modifyList head tail #' @importFrom stats na.omit @@ -557,6 +557,9 @@ tinyplot.default = function( xaxs = NULL, yaxs = NULL, ...) { + par_first = get_saved_par("first") + if (is.null(par_first)) set_saved_par("first", par()) + # save for tinyplot_add() if (!isTRUE(add)) { calls = sys.calls() @@ -590,6 +593,10 @@ tinyplot.default = function( palette = substitute(palette) + # themes + if (is.null(palette)) palette = get_tpar("palette", default = NULL) + if (is.null(pch)) pch = get_tpar("pch", default = NULL) + xlabs = ylabs = NULL # type_ridge() @@ -603,11 +610,19 @@ tinyplot.default = function( ## - set defaults of xaxt/yaxt (if these are NULL) based on axes ## - set logical axes based on xaxt/yaxt ## - set frame.plot default based on xaxt/yaxt - if (!is.character(axes)) axes = if (isFALSE(axes)) "none" else "standard" + if (isFALSE(axes)) { + axes = xaxt = yaxt = "none" + } else if (isTRUE(axes)) { + axes = "standard" + if (is.null(xaxt)) xaxt = get_tpar("xaxt", default = "standard") + if (is.null(yaxt)) yaxt = get_tpar("yaxt", default = "standard") + } else { + xaxt = yaxt = axes + } axis_types = c("standard", "none", "labels", "ticks", "axis") axes = match.arg(axes, axis_types) - if (is.null(xaxt)) xaxt = axes - if (is.null(yaxt)) yaxt = axes + xaxt = match.arg(xaxt, axis_types) + yaxt = match.arg(yaxt, axis_types) xaxt = substr(match.arg(xaxt, axis_types), 1L, 1L) yaxt = substr(match.arg(yaxt, axis_types), 1L, 1L) axes = any(c(xaxt, yaxt) != "n") @@ -959,24 +974,66 @@ tinyplot.default = function( if (is.null(legend_eval)) { legend_eval = tryCatch(paste0(legend)[[2]], error = function(e) NULL) } + adj_title = !is.null(legend) && (legend == "top!" || (!is.null(legend_args[["x"]]) && legend_args[["x"]] == "top!") || (is.list(legend_eval) && legend_eval[[1]] == "top!")) - if (is.null(main) || isFALSE(adj_title)) { - title( - main = main, - sub = sub - ) + + # For the "top!" legend case, bump main title up to make space for the + # legend beneath it: Take the normal main title line gap (i.e., 1.7 lines) + # and add the difference between original top margin and new one (i.e., + # which should equal the height of the new legend). Note that we also + # include a 0.1 epsilon bump, which we're using to reset the tinyplot + # window in case of recursive "top!" calls. (See draw_legend code.) + + if (isTRUE(adj_title)) { + line_main = par("mar")[3] - opar[["mar"]][3] + 1.7 + 0.1 } else { - # For the "top!" legend case, bump main title up to make space for the - # legend beneath it: Take the normal main title line gap (i.e., 1.7 lines) - # and add the difference between original top margin and new one (i.e., - # which should equal the height of the new legend). Note that we also - # include a 0.1 epsilon bump, which we're using to reset the tinyplot - # window in case of recursive "top!" calls. (See draw_legend code.) - title(main = main, line = par("mar")[3] - opar[["mar"]][3] + 1.7 + 0.1) - title(sub = sub) + line_main = NULL + } + + if (!is.null(sub)) { + if (isTRUE(get_tpar("side.sub", 1) == 3)) { + if (is.null(line_main)) line_main = par("mgp")[3] + 1.7 - .1 + line_main = line_main + 1.2 + } + if (isTRUE(get_tpar("side.sub", 1) == 3)) { + line_sub = get_tpar("line.sub", 1.7) + } else { + line_sub = get_tpar("line.sub", 4) + } + args = list( + text = sub, + line = line_sub, + cex = get_tpar("cex.sub", 1.2), + col = get_tpar("col.sub", "black"), + adj = get_tpar(c("adj.sub", "adj")), + font = get_tpar("font.sub", 1), + side = get_tpar("side.sub", 1), + las = 1 + ) + args = Filter(function(x) !is.null(x), args) + do.call(mtext, args) } + + if (!is.null(main)) { + args = list( + main = main, + line = line_main, + cex.main = get_tpar("cex.main", 1.4), + col.main = get_tpar("col.main", "black"), + font.main = get_tpar("font.main", 2), + adj = get_tpar(c("adj.main", "adj"), 3)) + args = Filter(function(x) !is.null(x), args) + do.call(title, args) + } + + # Axis titles - title(xlab = xlab, ylab = ylab) + args = list(xlab = xlab) + args[["adj"]] = get_tpar(c("adj.xlab", "adj")) + do.call(title, args) + args = list(ylab = ylab) + args[["adj"]] = get_tpar(c("adj.ylab", "adj")) + do.call(title, args) } # diff --git a/R/tinytheme.R b/R/tinytheme.R new file mode 100644 index 00000000..040f1fac --- /dev/null +++ b/R/tinytheme.R @@ -0,0 +1,348 @@ +#' Set or Reset Plot Themes for `tinyplot` +#' +#' @md +#' @description +#' The `tinytheme` function sets or resets the theme for plots created with +#' `tinyplot`. Themes control the appearance of plots, such as text alignment, +#' font styles, axis labels, and even dynamic margin adjustment to reduce +#' whitespace. +#' +#' @param theme A character string specifying the name of the theme to apply. +#' Themes are arranged in an approximate hierarchy, adding or subtracting +#' elements in the order presented below. Note that several themes are +#' _dynamic_, in the sense that they attempt to reduce whitespace in a way +#' that is responsive to the length of axes labels, tick marks, etc. These +#' dynamic plots are marked with an asterisk (*) below. +#' +#' - `"default"`: inherits the user's default base graphics settings. +#' - `"basic"`: light modification of `"default"`, only adding filled points, a panel background grid, and light gray background to facet titles. +#' - `"clean"` (*): builds on `"basic"` by moving the subtitle above the plotting area, adding horizontal axis labels, employing tighter default plot margins and title gaps to reduce whitespace, and setting different default palettes ("Tableau 10" for discrete colors and "agSunset" for gradient colors). The first of our dynamic themes and the foundation for several derivative themes that follow below. +#' - `"clean2"` (*): removes the plot frame (box) from `"clean"`, +#' - `"classic"` (*): connects the axes in a L-shape, but removes the other top and right-hand edges of the plot frame (box). Also sets the "Okabe-Ito" palette as a default for discrete colors. Inspired by the **ggplot2** theme of the same name. +#' - `"bw"` (*): similar to `"clean"`, except uses thinner lines for the plot frame (box), solid grid lines, and sets the "Okabe-Ito" palette as a default for discrete colors. Inspired by the **ggplot2** theme of the same name. +#' - `"ipsum"` (*): similar to `"bw"`, except subtitle is italicised and axes titles are aligned to the far edges. Inspired by the **hrbrthemes** theme of the same name for **ggplot2**. +#' - `"minimal"` (*): removes the plot frame (box) from `"bw"`, as well as the background for facet titles. Inspired by the **ggplot2** theme of the same name. +#' - `"dark"` (*): similar to `"minimal"`, but set against a dark background with foreground and a palette colours lightened for appropriate contrast. +#' - `"tufte"`: floating axes and minimalist plot artifacts in the style of Edward Tufte. +#' - `"void"`: switches off all axes, titles, legends, etc. +#' @param ... Named arguments to override specific theme settings. These +#' arguments are passed to `tpar()` and take precedence over the predefined +#' settings in the selected theme. +#' +#' @details +#' Sets a list of graphical parameters using `tpar()` +#' +#' To reset the theme to default settings (no customization), call `tinytheme()` +#' without arguments. +#' +#' **Cavear emptor:** Themes are a somewhat experimental feature of `tinyplot`. +#' We are reasonably confident that they should work as expected for most +#' "standard" cases. However, there may be some sharp edges. Please report any +#' unexpected behaviour to our GitHub repo: +#' https://github.com/grantmcdermott/tinyplot/issues +#' +#' Known current limitations include: +#' +#' - Themes do not work well when `legend = "top!"`. +#' - Themes do not play nicely with some complex plot types, particularly `"spineplot"` and `"ridge"`. +#' - Dynamic margin spacing does not account for multi-line strings (e.g., axes +#' or main titles that contain `"\n"`). +#' +#' @return The function returns nothing. It is called for its side effects. +#' +#' @seealso [tpar] which does the heavy lifting under the hood. +#' +#' @examples +#' +#' # Reusable plot function +#' p = function() tinyplot( +#' lat ~ long | depth, data = quakes, +#' main = "Earthquakes off Fiji", +#' sub = "Data courtesy of the Harvard PRIM-H project" +#' ) +#' p() +#' +#' # Set a theme +#' tinytheme("bw") +#' p() +#' +#' # Try a different theme +#' tinytheme("dark") +#' p() +#' +#' # Customize the theme by overriding default settings +#' tinytheme("bw", fg = "green", font.main = 2, font.sub = 3, family = "Palatino") +#' p() +#' +#' # Reset the theme +#' tinytheme() +#' p() +#' +#' # Themes showcase +#' ## We'll use a slightly more intricate plot (long y-axis labs and facets) +#' ## to demonstrate dynamic margin adjustment etc. +#' +#' thms = eval(formals(tinytheme)$theme) +#' +#' for (thm in thms) { +#' tinytheme(thm) +#' tinyplot( +#' I(Sepal.Length*1e4) ~ Petal.Length | Species, facet = "by", data = iris, +#' main = "Demonstration of tinyplot themes", +#' sub = paste0('tinytheme("', thm, '")') +#' ) +#' } +#' +#' # Reset +#' tinytheme() +#' +#' @export +tinytheme = function( + theme = c( + "default", "basic", + "clean", "clean2", "bw", "classic", + "minimal", "ipsum", "dark", + "tufte", "void" + ), + ... + ) { + + theme = match.arg(theme) + + # in notebooks, we don't want to close the device because no image. + # init_tpar() tries to be smart, but may fail. + init_tpar(rm_hook = TRUE) + + assert_choice( + theme, + c( + "default", + sort(c("basic", "bw", "classic", "clean", "clean2", "dark", "ipsum", "minimal", "tufte", "void")) + ) + ) + + settings = switch(theme, + "default" = theme_default, + "basic" = theme_basic, + "bw" = theme_bw, + "classic" = theme_classic, + "clean" = theme_clean, + "clean2" = theme_clean2, + "dark" = theme_dark, + "ipsum" = theme_ipsum, + "minimal" = theme_minimal, + "tufte" = theme_tufte, + "void" = theme_void, + ) + + dots = list(...) + for (n in names(dots)) { + settings[[n]] = dots[[n]] + } + + if (length(settings) > 0) { + if (theme == "default") { + # for default theme, we want to revert the original pars and turn off the + # before.new.plot hook (otherwise manual par(x = y) changes won't work) + tpar(settings, hook = FALSE) + setHook("before.new.plot", NULL, "replace") + } else { + tpar(settings, hook = TRUE) + } + } + + return(invisible(NULL)) +} + + + +# +## Themes (these are read and set at initial load time) + +# theme_default = list() + +theme_default = list( + tinytheme = "default", + adj = par("adj"), # 0.5, + adj.main = par("adj"), # 0.5, + adj.sub = par("adj"), # 0.5, + bg = par("bg"), # "white" + bty = par("bty"), #"o", + cex.axis = par("cex.axis"), #1, + cex.main = par("cex.main"), #1.2, + cex.xlab = par("cex.axis"), #1, + cex.ylab = par("cex.axis"), #1, + col.axis = par("col.axis"), #1, + col.xaxs = par("col.axis"), #1, + col.yaxs = par("col.axis"), #1, + col.lab = par("col.lab"), #"black", + col.main = par("col.main"), #"black", + col.sub = par("col.sub"), #"black", + dynmar = FALSE, + facet.bg = NULL, + facet.border = NA, + family = par("family"), # "" + fg = par("fg"), + font = par("font"), # 1, + font.axis = par("font.axis"), # 1, + font.lab = par("font.lab"), # 1, + font.main = par("font.main"), # 2, + font.sub = par("font.sub"), # 2, + grid = FALSE, + grid.col = "lightgray", + grid.lty = "dotted", + grid.lwd = 1, + lab = par("lab"), # c(5, 5, 7), + las = par("las"), # 0, + lwd = par("lwd"), # 1, + lwd.axis = par("lwd"), # 1, + mar = c(5.1, 4.1, 4.1, 2.1), ## test + mgp = par("mgp"), + # palette.qualitative = "R4", + # palette.sequential = "ag_Sunset", + pch = par("pch"), # 1, + side.sub = 1, + tck = NA, + xaxt = "standard", + yaxt = "standard" +) + +# derivatives of "default" +# - basic +# - tufte +# - void + +theme_basic = modifyList(theme_default, list( + tinytheme = "basic", + facet.bg = "gray90", + facet.border = "black", + grid = TRUE, + pch = 16 +)) + +theme_tufte = modifyList(theme_default, list( + tinytheme = "tufte", + adj.main = 0, + adj.sub = 0, + bty = "n", + font.main = 1, + lab = c(10, 10, 7), + # palette.sequential = "Grays", + pch = 16, + side.sub = 3, + tcl = 0.2 +)) + +theme_void = modifyList(theme_default, list( + tinytheme = "void", + adj.main = 0, + adj.sub = 0, + font.main = 1, + palette.qualitative = "Tableau 10", + palette.sequential = "ag_Sunset", + pch = 16, + side.sub = 3, + # tck = -.02, + xaxt = "none", + yaxt = "none" +)) + +# derivatives of "basic" +# - clean + +theme_clean = modifyList(theme_basic, list( + ## Notes: + ## - 1. Reduce axis title gap by 0.5 lines and also reduce tcl to 0.3 lines. + ## - 2. Sub moves to top. + ## - 3. Also want to remove excess white on rhs of plot margin (when no legend). + ## - Together, 1, 2, and 3 imply that... + ## -- mgp[1] should be adjusted by 0.8 (= 0.5 + 0.3) + ## -- mgp[2] should be adjusted by 0.3 + ## -- mar[1] should be adjusted by 1.8 (= 1 (no sub) + 0.5 + 0.3 (tighter axis labs)) + ## -- mar[2] should be adjusted by 0.8 (= 0.5 + 0.3) + ## -- mar[3] should remain unchanged (main + sub will adjust automatically) + ## -- mar[4] should be adjusted by 1.5 (relative to 2.1) + ## + tinytheme = "clean", + adj.main = 0, + adj.sub = 0, + dynmar = TRUE, + las = 1, + mar = c(5.1, 4.1, 4.1, 2.1) - c(1+0.5+0.3, 0.5+0.3, 0, 1.5), ## test + mgp = c(3, 1, 0) - c(0.5+0.3, 0.3, 0), # i.e., subtract 0.5 lines + the (abs) value of the tcl adjustment + palette.qualitative = "Tableau 10", + palette.sequential = "ag_Sunset", + side.sub = 3, + tcl = -0.3 +)) + +# derivatives of "clean" +# - clean2 +# - classic +# - bw + +theme_clean2 = modifyList(theme_clean, list( + tinytheme = "clean2", + facet.border = "gray90", + xaxt = "labels", + yaxt = "labels" +)) + +theme_classic = modifyList(theme_clean, list( + tinytheme = "classic", + bty = "l", + facet.bg = NULL, + font.main = 1, + grid = FALSE, + palette.qualitative = "Okabe-Ito" +)) + +theme_bw = modifyList(theme_clean, list( + tinytheme = "bw", + font.main = 1, + grid.lty = 1, + grid.lwd = 0.5, + lwd = 0.5, + lwd.axis = 0.5, + palette.qualitative = "Okabe-Ito" +)) + +# derivatives of "bw" +# - minimal +# - ipsum +# - dark + +theme_minimal = modifyList(theme_bw, list( + tinytheme = "minimal", + bty = "n", + facet.bg = NULL, + facet.border = NULL, + xaxt = "labels", + yaxt = "labels" +)) + +theme_ipsum = modifyList(theme_minimal, list( + tinytheme = "ipsum", + bty = "n", + font.sub = 3, + adj.ylab = 1, + adj.xlab = 1 +)) + +theme_dark = modifyList(theme_minimal, list( + tinytheme = "dark", + bg = "#1A1A1A", + fg = "#BBBBBB", + # col = "white", + col.xaxs = "#BBBBBB", + col.yaxs = "#BBBBBB", + col.lab = "#BBBBBB", + col.main = "#BBBBBB", + col.sub = "#BBBBBB", + col.axis = "#BBBBBB", + # facet.bg = "gray20", + grid.col = "#6D6D6D", + palette.qualitative = "Set 2", + palette.sequential = "Sunset" +)) + diff --git a/R/tpar.R b/R/tpar.R index e8ab3c82..d6fae9fe 100644 --- a/R/tpar.R +++ b/R/tpar.R @@ -1,5 +1,5 @@ #' @title Set or query graphical parameters -#' +#' #' @description Extends \code{\link[graphics]{par}}, serving as a (near) drop-in #' replacement for setting or querying graphical parameters. The key #' differences is that, beyond supporting the standard group of R graphical @@ -8,13 +8,15 @@ #' \code{\link[graphics]{par}}, parameters are set by passing appropriate #' `key = value` argument pairs, and multiple parameters can be set or queried #' at the same time. -#' -#' @md +#' #' @param ... arguments of the form `key = value`. This includes all of the #' parameters typically supported by \code{\link[graphics]{par}}, as well as #' the `tinyplot`-specific ones described in the 'Graphical Parameters' #' section below. -#' +#' @param hook Logical. If `TRUE`, base graphical parameters persist across +#' plots via a hook applied before each new plot (see `?setHook`). +#' +#' @md #' @details The `tinyplot`-specific parameters are saved in an internal #' environment called `.tpar` for performance and safety reasons. However, #' they can also be set at package load time via \code{\link[base]{options}}, @@ -22,260 +24,321 @@ #' behaviour at startup (e.g., through an `.Rprofile` file). These options all #' take a `tinyplot_*` prefix, e.g. #' `options(tinyplot_grid = TRUE, tinyplot_facet.bg = "grey90")`. -#' +#' #' For their part, any "base" graphical parameters are caught dynamically and #' passed on to \code{\link[graphics]{par}} as appropriate. Technically, only #' parameters that satisfy `par(..., no.readonly = TRUE)` are evaluated. -#' +#' #' However, note the important distinction: `tpar` only evaluates parameters #' from \code{\link[graphics]{par}} if they are passed _explicitly_ by the #' user. This means that `tpar` should not be used to capture the (invisible) #' state of a user's entire set of graphics parameters, i.e. `tpar()` != #' `par()`. If you want to capture the _all_ existing graphics settings, then -#' you should rather use `par()` instead. -#' +#' you should rather use `par()` instead. +#' #' @returns When parameters are set, their previous values are returned in an #' invisible named list. Such a list can be passed as an argument to `tpar` to #' restore the parameter values. -#' +#' #' When just one parameter is queried, the value of that parameter is returned #' as (atomic) vector. When two or more parameters are queried, their values #' are returned in a list, with the list names giving the parameters. -#' +#' #' Note the inconsistency: setting one parameter returns a list, but querying #' one parameter returns a vector. #' #' @section Additional Graphical Parameters: -#' -#' \tabular{lll}{ -#' `facet.cex` \tab\tab Expansion factor for facet titles. Defaults to `1`.\cr -#' \tab\tab\cr -#' \tab\tab\cr -#' `facet.font` \tab\tab An integer corresponding to the desired font face for facet titles. For most font families and graphics devices, one of four possible values: `1` (regular), `2` (bold), `3` (italic), or `4` (bold italic). Defaults to `NULL`, which is equivalent to `1` (i.e., regular).\cr -#' \tab\tab\cr -#' \tab\tab\cr -#' `facet.col` \tab\tab Character or integer specifying the facet text colour. If an integer, will correspond to the user's default global colour palette (see \code{\link[grDevices]{palette}}). Defaults to `NULL`, which is equivalent to "black".\cr -#' \tab\tab\cr -#' \tab\tab\cr -#' `facet.bg` \tab\tab Character or integer specifying the facet background colour. If an integer, will correspond to the user's default colour palette (see \code{\link[grDevices]{palette}}). Passed \code{\link[graphics]{rect}}. Defaults to `NULL` (none).\cr -#' \tab\tab\cr -#' \tab\tab\cr -#' `facet.border` \tab\tab Character or integer specifying the facet border colour. If an integer, will correspond to the users default colour palette (see \code{\link[grDevices]{palette}}). Passed \code{\link[graphics]{rect}}. Defaults to `NA` (none).\cr -#' \tab\tab\cr -#' \tab\tab\cr -#' `file.height` \tab\tab Numeric specifying the height (in inches) of any plot that is written to disk using the `tinyplot(..., file = X)` argument. Defaults to 7.\cr -#' \tab\tab\cr -#' \tab\tab\cr -#' `file.width` \tab\tab Numeric specifying the width (in inches) of any plot that is written to disk using the `tinyplot(..., file = X)` argument. Defaults to 7.\cr -#' \tab\tab\cr -#' \tab\tab\cr -#' `file.res` \tab\tab Numeric specifying the resolution (in dots per square inch) of any plot that is written to disk in bitmap format (i.e., PNG or JPEG) using the `tinyplot(..., file = X)` argument. Defaults to 300.\cr -#' \tab\tab\cr -#' \tab\tab\cr -#' `fmar` \tab\tab A numeric vector of form `c(b,l,t,r)` for controlling the (base) margin padding, in terms of lines, between the individual facets in a faceted plot. Defaults to `c(1,1,1,1)`, i.e. a single line of padding around each facet. If more that three facets are detected, the `fmar` parameter is scaled by 0.75 (i.e., three-quarters) to reduce the excess whitespace that would otherwise arise due to the absent axes lines and labels. (An exception is made for 2x2 plots to better match the `cex` expansion logic of the base graphics system under this particular layout.) Similarly, note that an extra 0.5 lines is subtracted from each side of the facet padding for plots that aren't framed, to reduce excess whitespace.\cr -#' \tab\tab\cr -#' \tab\tab\cr -#' `grid` \tab\tab Logical indicating whether a background panel grid should be added to plots automatically. Defaults to NULL, which is equivalent to `FALSE`.\cr -#' \tab\tab\cr -#' \tab\tab\cr -#' `grid.col` \tab\tab Character or (integer) numeric that specifies the color of the panel grid lines. Defaults to `"lightgray"`.\cr -#' \tab\tab\cr -#' \tab\tab\cr -#' `grid.lty` \tab\tab Character or (integer) numeric that specifies the line type of the panel grid lines. Defaults to `"dotted"`.\cr -#' \tab\tab\cr -#' \tab\tab\cr -#' `grid.lwd` \tab\tab Non-negative numeric giving the line of the panel grid lines. Defaults to `1`.\cr -#' \tab\tab\cr -#' \tab\tab\cr -#' `lmar` \tab\tab A numeric vector of form `c(inner, outer)` that gives the margin padding, in terms of lines, around the automatic `tinyplot` legend. Defaults to `c(1.0, 0.1)`, where the first number represents the "inner" margin between the legend and the plot region, and the second number represents the "outer" margin between the legend and edge of the graphics device. (Note that an exception for the definition of the "outer" legend margin occurs when the legend placement is `"top!"`, since the legend is placed above the plot region but below the main title. In such cases, the outer margin is relative to the existing gap between the title and the plot region, which is itself determined by `par("mar")[3]`.)\cr -#' \tab\tab\cr -#' \tab\tab\cr -#' `ribbon.alpha` \tab\tab Numeric factor in the range `[0,1]` for modifying the opacity alpha of "ribbon" and "area" (and alike) type plots. Default value is `0.2`.\cr -#' } -#' +#' +#' * `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. +#' * `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). +#' * `facet.cex`: Expansion factor for facet titles. Defaults to `1`. +#' * `facet.col`: Character or integer specifying the facet text colour. If an integer, will correspond to the user's default global colour palette (see `palette`). Defaults to `NULL`, which is equivalent to "black". +#' * `facet.font`: An integer corresponding to the desired font face for facet titles. For most font families and graphics devices, one of four possible values: `1` (regular), `2` (bold), `3` (italic), or `4` (bold italic). Defaults to `NULL`, which is equivalent to `1` (i.e., regular). +#' * `file.height`: Numeric specifying the height (in inches) of any plot that is written to disk using the `tinyplot(..., file = X)` argument. Defaults to `7`. +#' * `file.res`: Numeric specifying the resolution (in dots per square inch) of any plot that is written to disk in bitmap format (i.e., PNG or JPEG) using the `tinyplot(..., file = X)` argument. Defaults to `300`. +#' * `file.width`: Numeric specifying the width (in inches) of any plot that is written to disk using the `tinyplot(..., file = X)` argument. Defaults to `7`. +#' * `fmar`: A numeric vector of form `c(b,l,t,r)` for controlling the (base) margin padding, in terms of lines, between the individual facets in a faceted plot. Defaults to `c(1,1,1,1)`. If more than three facets are detected, the `fmar` parameter is scaled by 0.75 to reduce excess whitespace. For 2x2 plots, the padding better matches the `cex` expansion logic of base graphics. +#' * `grid.col`: Character or (integer) numeric that specifies the color of the panel grid lines. Defaults to `"lightgray"`. +#' * `grid.lty`: Character or (integer) numeric that specifies the line type of the panel grid lines. Defaults to `"dotted"`. +#' * `grid.lwd`: Non-negative numeric giving the line width of the panel grid lines. Defaults to `1`. +#' * `grid`: Logical indicating whether a background panel grid should be added to plots automatically. Defaults to `NULL`, which is equivalent to `FALSE`. +#' * `lmar`: A numeric vector of form `c(inner, outer)` that gives the margin padding, in terms of lines, around the automatic `tinyplot` legend. Defaults to `c(1.0, 0.1)`. The inner margin is the gap between the legend and the plot region, and the outer margin is the gap between the legend and the edge of the graphics device. +#' * `palette.qualitative`: Palette for qualitative colors. See the `palette` argumetn in `?tinyplot`. +#' * `palette.sequential`: Palette for sequential colors. See the `palette` argumetn in `?tinyplot`. +#' * `ribbon.alpha`: Numeric factor in the range `[0,1]` for modifying the opacity alpha of "ribbon" and "area" type plots. Default value is `0.2`. +#' #' @importFrom graphics par #' @importFrom utils modifyList #' +#' @seealso [graphics::par] which `tpar` builds on top of. [get_saved_par] is +#' a convenience function for retrieving graphical parameters at different +#' stages of a `tinyplot` call (and used for internal accounting purposes). +#' [tinytheme] allows users to easily set a group of graphics parameters +#' in a single function call, according to a variety of predefined themes. +#' #' @examples #' # Return a list of existing base and tinyplot graphic params #' tpar("las", "pch", "facet.bg", "facet.cex", "grid") -#' +#' #' # Simple facet plot with these default values #' tinyplot(mpg ~ wt, data = mtcars, facet = ~am) -#' +#' #' # Set params to something new. Similar to graphics::par(), note that we save #' # the existing values at the same time by assigning to an object. #' op = tpar( -#' las = 1, -#' pch = 2, -#' facet.bg = "grey90", -#' facet.cex = 2, -#' grid = TRUE +#' las = 1, +#' pch = 2, +#' facet.bg = "grey90", +#' facet.cex = 2, +#' grid = TRUE #' ) -#' +#' #' # Re-plot with these new params #' tinyplot(mpg ~ wt, data = mtcars, facet = ~am) -#' +#' #' # Reset back to original values #' tpar(op) -#' +#' #' # Important: tpar() only evalutes parameters that have been passed explicitly #' # by the user. So it it should not be used to query and set (restore) #' # parameters that weren't explicitly requested, i.e. tpar() != par(). -#' +#' #' # Note: The tinyplot-specific parameters can also be be set via `options` #' # with a `tinyplot_*` prefix, which can be convenient for enabling #' # different default behaviour at startup time (e.g., via an .Rprofile #' # file). Example: #' # options(tinyplot_grid = TRUE, tinyplot_facet.bg = "grey90") -#' +#' #' @export -tpar = function(...) { - - facet.col = facet.bg = facet.border = used_par_old = NULL - +tpar = function(..., hook = FALSE) { + opts = list(...) - if (length(opts)==1 && is.null(names(opts))) { + if (length(opts) == 1 && is.null(names(opts))) { if (inherits(opts[[1]], "list") && !is.null(names(opts[[1]]))) { opts = opts[[1]] } } - - tpar_old = as.list(.tpar) + + ###### Assign parameters + + # assign tinyplot-specific arguments with known names to .tpar + assign_tpar(opts) + + # return informative error messages if the input is invalid + assert_tpar(.tpar) + + # if tpar(...) includes arguments that are not known to be tinyplot-specific, + # we set a hook to set them using par() when the graphic device is started nam = names(opts) - - known_par = names(par(no.readonly = TRUE)) if (!is.null(nam)) { - used_par = intersect(nam, known_par) - } else { - used_par = intersect(opts, known_par) - } - if (length(used_par)) { - if (!is.null(nam)) used_par = opts[used_par] - used_par_old = par(used_par) - tpar_old = modifyList(tpar_old, used_par_old, keep.null = TRUE) - } - - if (length(opts$facet.cex)) { - facet.cex = as.numeric(opts$facet.cex) - if(!is.numeric(facet.cex)) stop("facet.cex needs to be numeric") - if(length(facet.cex)!=1) stop("facet.cex needs to be of length 1") - .tpar$facet.cex = facet.cex - } - - if (length(opts$facet.font)) { - facet.font = as.numeric(opts$facet.font) - if(!is.numeric(facet.font)) stop("facet.font needs to be numeric") - if(length(facet.font)!=1) stop("facet.font needs to be of length 1") - .tpar$facet.font = facet.font - } - - if (length(opts$facet.col) || ("facet.col" %in% nam && is.null(opts$facet.col))) { - facet.col = opts$facet.col - if(!is.null(facet.col) && !is.numeric(facet.col) && !is.character(facet.col)) stop("facet.col needs to be NULL, or a numeric or character") - if(!is.null(facet.col) && length(facet.col)!=1) stop("facet.col needs to be of length 1") - .tpar$facet.col = facet.col - } - - if (length(opts$facet.bg) || ("facet.bg" %in% nam && is.null(opts$facet.bg))) { - facet.bg = opts$facet.bg - if(!is.null(facet.bg) && !is.numeric(facet.bg) && !is.character(facet.bg)) stop("facet.bg needs to be NULL, or a numeric or character") - if(!is.null(facet.bg) && length(facet.bg)!=1) stop("facet.bg needs to be of length 1") - .tpar$facet.bg = facet.bg - } - - if (length(opts$facet.border)) { - facet.border = opts$facet.border - if(!is.na(facet.border) && !is.numeric(facet.border) && !is.character(facet.border)) stop("facet.border needs to be NA, or a numeric or character") - if(length(facet.border)!=1) stop("facet.border needs to be of length 1") - .tpar$facet.border = facet.border - } - - if (length(opts$file.width)) { - file.width = as.numeric(opts$file.width) - if(!is.numeric(file.width)) stop("file.width needs to be numeric") - .tpar$file.width = file.width - } - - if (length(opts$file.height)) { - file.height = as.numeric(opts$file.height) - if(!is.numeric(file.height)) stop("file.height needs to be numeric") - .tpar$file.height = file.height - } - - if (length(opts$file.res)) { - file.res = as.numeric(opts$file.res) - if(!is.numeric(file.res)) stop("file.res needs to be numeric") - .tpar$file.res = file.res - } - - if (length(opts$fmar)) { - fmar = as.numeric(opts$fmar) - if(!is.numeric(fmar)) stop("fmar needs to be numeric") - if(length(fmar)!=4) stop("fmar needs to be of length 4, i.e. c(b,l,t,r)") - .tpar$fmar = fmar - } - - if (length(opts$grid)) { - grid = as.logical(opts$grid) - if(!is.null(grid) && !is.logical(grid)) stop("grid needs to be NULL or logical") - .tpar$grid = grid - } - - if (length(opts$grid.col)) { - grid.col = opts$grid.col - .tpar$grid.col = grid.col - } - - if (length(opts$grid.lty)) { - grid.lty = opts$grid.lty - .tpar$grid.lty = grid.lty - } - - if (length(opts$grid.lwd)) { - grid.lwd = as.numeric(opts$grid.lwd) - if(!is.numeric(grid.lwd) || grid.lwd < 0) stop("grid.lwd needs to be a non-negative numeric") - .tpar$grid.lwd = grid.lwd - } - - - if (length(opts$lmar)) { - lmar = as.numeric(opts$lmar) - if(!is.numeric(lmar)) stop("lmar needs to be numeric") - if(length(lmar)!=2) stop("lmar needs to be of length 2, i.e. c(inner, outer)") - .tpar$lmar = lmar - } - - if (length(opts$ribbon.alpha)) { - ribbon.alpha = as.numeric(opts$ribbon.alpha) - if (!is.numeric(ribbon.alpha) || ribbon.alpha<0 || ribbon.alpha>1) stop("ribbon.alpha needs to be a numeric in the range [0,1]") - .tpar$ribbon.alpha = ribbon.alpha + base_par = setdiff(nam, known_tpar) + base_par = opts[base_par] + if (length(base_par) > 0) { + if (isTRUE(hook)) { + setHook("before.plot.new", function() par(base_par), action = "replace") + } else { + par_names = names(par(no.readonly = TRUE)) + base_par = base_par[names(base_par) %in% par_names] + par(base_par) + } + } } - - ## Like par(), we want the return object to be dependent on inputs... - + + + ###### Retrieve parameters + # User didn't assign any new values, but may have requested explicit (print # of) some existing value(s) + tpar_old = as.list(.tpar) if (is.null(nam)) { - if (!is.null(opts) && length(opts)!=0) { + known_par = names(par(no.readonly = TRUE)) + if (!is.null(nam)) { + used_par = intersect(nam, known_par) + } else { + used_par = intersect(opts, known_par) + } + if (length(used_par)) { + if (!is.null(nam)) used_par = opts[used_par] + used_par_old = par(used_par) + tpar_old = modifyList(as.list(.tpar), used_par_old, keep.null = TRUE) + } + if (!is.null(opts) && length(opts) != 0) { # specific values requested + opts = Filter(is.character, opts) ret = (`names<-`(lapply(opts, function(x) .tpar[[x]]), opts)) if (length(used_par)) { ret_par = par(used_par) ret = modifyList(ret, ret_par, keep.null = TRUE) } - if (length(ret)==1) ret = ret[[1]] + if (length(ret) == 1) ret = ret[[1]] return(ret) } else { # no specific request; return all existing values invisibly return(invisible(tpar_old)) } - # assign new values, but still return old values for saving existing settings - # a la `oldpar = par(param = new_value)` + # assign new values, but still return old values for saving existing settings + # a la `oldpar = par(param = new_value)` } else { `names<-`(lapply(nam, function(x) .tpar[[x]]), nam) return(invisible(tpar_old)) } - } + +# Two levels of priority: .tpar[["name"]] -> par("name") +get_tpar = function(opts, default = NULL) { + # parameter priority + # .tpar[["name"]] -> par("name") + for (o in opts) { + tp = .tpar[[o]] + if (!is.null(tp)) { + return(tp) + } else { + p = suppressWarnings(par(o)) + if (!is.null(p)) { + return(p) + } + } + + } + return(default) +} + + +known_tpar = c( + "adj.main", + "adj.sub", + "adj.xlab", + "adj.ylab", + "cex.xlab", + "cex.ylab", + "col.xaxs", + "col.yaxs", + "dynmar", + "facet.bg", + "facet.border", + "facet.cex", + "facet.col", + "facet.font", + "file.height", + "file.res", + "file.width", + "fmar", + "grid", + "grid.bg", + "grid.col", + "grid.lty", + "grid.lwd", + "lmar", + "lty.xaxs", + "lty.yaxs", + "lwd.xaxs", + "lwd.yaxs", + "lwd.axis", + "pch", + "palette.qualitative", + "palette.sequential", + "ribbon.alpha", + "side.sub", + "tinytheme", + "xaxt", + "yaxt" +) + + +assign_tpar = function(opts) { + for (n in intersect(names(opts), known_tpar)) { + .tpar[[n]] = opts[[n]] + } +} + + +assert_tpar = function(.tpar) { + 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") + assert_numeric(.tpar[["adj.ylab"]], len = 1, lower = 0, upper = 1, null.ok = TRUE, name = "adj.ylab") + assert_flag(.tpar[["dynmar"]], null.ok = FALSE, name = "dynmar") + assert_numeric(.tpar[["lmar"]], len = 2, null.ok = TRUE, name = "lmar") + assert_numeric(.tpar[["ribbon.alpha"]], len = 1, lower = 0, upper = 1, null.ok = TRUE, name = "ribbon.alpha") + assert_numeric(.tpar[["grid.lwd"]], len = 1, lower = 0, null.ok = TRUE, name = "grid.lwd") + assert_flag(.tpar[["grid"]], null.ok = TRUE, name = "grid") + assert_numeric(.tpar[["file.res"]], len = 1, lower = 0, null.ok = TRUE, name = "file.res") + assert_numeric(.tpar[["file.height"]], len = 1, lower = 0, null.ok = TRUE, name = "file.height") + assert_numeric(.tpar[["file.width"]], len = 1, lower = 0, null.ok = TRUE, name = "file.width") + assert_numeric(.tpar[["facet.font"]], len = 1, null.ok = TRUE, name = "facet.font") + assert_numeric(.tpar[["facet.cex"]], len = 1, null.ok = TRUE, name = "facet.cex") + assert_numeric(.tpar[["side.sub"]], len = 1, null.ok = TRUE, name = "side.sub") + assert_string(.tpar[["grid.bg"]], null.ok = TRUE, name = "grid.bg") + assert_numeric(.tpar[["fmar"]], len = 4, null.ok = TRUE, name = "fmar") + + facet.col = .tpar[["facet.col"]] + if (!is.null(facet.col)) { + if (!is.null(facet.col) && !is.numeric(facet.col) && !is.character(facet.col)) { + stop("facet.col needs to be NULL, or a numeric or character", call. = FALSE) + } + assert_true(length(facet.col) == 1, name = "length(facet.col)==1") + } + + facet.bg = .tpar$facet.bg + if (!is.null(facet.bg)) { + if (!is.numeric(facet.bg) && !is.character(facet.bg)) { + stop("facet.bg needs to be NULL, or a numeric or character", call. = FALSE) + } + assert_true(length(facet.bg) == 1, name = "length(facet.bg)==1") + } + + facet.border = .tpar$facet.border + if (!is.null(facet.border)) { + if (!is.numeric(facet.border) && !is.character(facet.border) && !is.na(facet.border)) { + stop("facet.border needs to be NULL, or a numeric, character, or NA", call. = FALSE) + } + assert_true(length(facet.border) == 1, name = "length(facet.border)==1") + } +} + + +init_tpar = function(rm_hook = FALSE) { + rm(list = names(.tpar), envir = .tpar) + + if (isTRUE(rm_hook)) { + hook = getHook("before.plot.new") + if (length(hook) > 0) { + # need weird function because of Quarto evaluate::evaluate failure + # setHook("before.plot.new", NULL, action = "replace") + setHook("before.plot.new", function() NULL, action = "replace") + } + } + + .tpar$dynmar = if (is.null(getOption("tinyplot_dynmar"))) FALSE else as.logical(getOption("tinyplot_dynmar")) + + # Figure output options if written to file + .tpar$file.width = if (is.null(getOption("tinyplot_file.width"))) 7 else as.numeric(getOption("tinyplot_file.width")) + .tpar$file.height = if (is.null(getOption("tinyplot_file.height"))) 7 else as.numeric(getOption("tinyplot_file.height")) + .tpar$file.res = if (is.null(getOption("tinyplot_file.res"))) 300 else as.numeric(getOption("tinyplot_file.res")) + + # Facet margin, i.e. gap between the individual facet windows + .tpar$fmar = if (is.null(getOption("tinyplot_fmar"))) c(1, 1, 1, 1) else as.numeric(getOption("tinyplot_fmar")) + + # Other facet options + .tpar$facet.cex = if (is.null(getOption("tinyplot_facet.cex"))) 1 else as.numeric(getOption("tinyplot_facet.cex")) + .tpar$facet.font = if (is.null(getOption("tinyplot_facet.font"))) NULL else as.numeric(getOption("tinyplot_facet.font")) + .tpar$facet.col = if (is.null(getOption("tinyplot_facet.col"))) NULL else getOption("tinyplot_facet.col") + .tpar$facet.bg = if (is.null(getOption("tinyplot_facet.bg"))) NULL else getOption("tinyplot_facet.bg") + .tpar$facet.border = if (is.null(getOption("tinyplot_facet.border"))) NA else getOption("tinyplot_facet.border") + + # Plot grid + .tpar$grid = if (is.null(getOption("tinyplot_grid"))) FALSE else as.logical(getOption("tinyplot_grid")) + .tpar$grid.col = if (is.null(getOption("tinyplot_grid.col"))) "lightgray" else getOption("tinyplot_grid.col") + .tpar$grid.lty = if (is.null(getOption("tinyplot_grid.lty"))) "dotted" else getOption("tinyplot_grid.lty") + .tpar$grid.lwd = if (is.null(getOption("tinyplot_grid.lwd"))) 1 else as.numeric(getOption("tinyplot_grid.lwd")) + + # Legend margin, i.e. gap between the legend and the plot elements + .tpar$lmar = if (is.null(getOption("tinyplot_lmar"))) c(1.0, 0.1) else as.numeric(getOption("tinyplot_lmar")) + + # Alpha fill (transparency) default for ribbon and area plots + .tpar$ribbon.alpha = if (is.null(getOption("tinyplot_ribbon.alpha"))) 0.2 else as.numeric(getOption("tinyplot_ribbon.alpha")) +} diff --git a/R/type_ridge.R b/R/type_ridge.R index 49a23ed2..f6a7cda4 100644 --- a/R/type_ridge.R +++ b/R/type_ridge.R @@ -381,7 +381,8 @@ segmented_raster = function(x, y, ymin = 0, breaks = range(x), probs = NULL, man } ## auxiliary function for determining quantiles based on density function -#' @importFrom stats approx median + +#' @importFrom stats median approx quantile.density = function(x, probs = seq(0, 1, 0.25), ...) { ## sanity check for probabilities if (any(probs < 0 | probs > 1)) stop("'probs' outside [0,1]") diff --git a/R/zzz.R b/R/zzz.R index b1e4b27f..b8a375ca 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -5,45 +5,19 @@ #' @keywords internal #' @noRd .onLoad = function(libname, pkgname) { - # https://stackoverflow.com/questions/12598242/global-variables-in-packages-in-r # https://stackoverflow.com/questions/49056642/r-how-to-make-variable-available-to-namespace-at-loading-time?noredirect=1&lq=1 tnypltptns = parent.env(environment()) assign(".tinyplot_env", new.env(), envir = tnypltptns) .tpar = new.env() - - # Figure output options if written to file - .tpar$file.width = if(is.null(getOption("tinyplot_file.width"))) 7 else as.numeric(getOption("tinyplot_file.width")) - .tpar$file.height = if(is.null(getOption("tinyplot_file.height"))) 7 else as.numeric(getOption("tinyplot_file.height")) - .tpar$file.res = if(is.null(getOption("tinyplot_file.res"))) 300 else as.numeric(getOption("tinyplot_file.res")) - - # Facet margin, i.e. gap between the individual facet windows - .tpar$fmar = if(is.null(getOption("tinyplot_fmar"))) c(1,1,1,1) else as.numeric(getOption("tinyplot_fmar")) - - # Other facet options - .tpar$facet.cex = if(is.null(getOption("tinyplot_facet.cex"))) 1 else as.numeric(getOption("tinyplot_facet.cex")) - .tpar$facet.font = if(is.null(getOption("tinyplot_facet.font"))) NULL else as.numeric(getOption("tinyplot_facet.font")) - .tpar$facet.col = if(is.null(getOption("tinyplot_facet.col"))) NULL else getOption("tinyplot_facet.col") - .tpar$facet.bg = if(is.null(getOption("tinyplot_facet.bg"))) NULL else getOption("tinyplot_facet.bg") - .tpar$facet.border = if(is.null(getOption("tinyplot_facet.border"))) NA else getOption("tinyplot_facet.border") - - # Plot grid - .tpar$grid = if(is.null(getOption("tinyplot_grid"))) FALSE else as.logical(getOption("tinyplot_grid")) - .tpar$grid.col = if(is.null(getOption("tinyplot_grid.col"))) "lightgray" else getOption("tinyplot_grid.col") - .tpar$grid.lty = if(is.null(getOption("tinyplot_grid.lty"))) "dotted" else getOption("tinyplot_grid.lty") - .tpar$grid.lwd = if(is.null(getOption("tinyplot_grid.lwd"))) 1 else as.numeric(getOption("tinyplot_grid.lwd")) - - # Legend margin, i.e. gap between the legend and the plot elements - .tpar$lmar = if(is.null(getOption("tinyplot_lmar"))) c(1.0, 0.1) else as.numeric(getOption("tinyplot_lmar")) - - # Alpha fill (transparency) default for ribbon and area plots - .tpar$ribbon.alpha = if(is.null(getOption("tinyplot_ribbon.alpha"))) 0.2 else as.numeric(getOption("tinyplot_ribbon.alpha")) - + assign(".tpar", .tpar, envir = tnypltptns) - - + + init_tpar() + assign(".saved_par_before", NULL, envir = get(".tinyplot_env", envir = parent.env(environment()))) assign(".saved_par_after", NULL, envir = get(".tinyplot_env", envir = parent.env(environment()))) + assign(".saved_par_first", NULL, envir = get(".tinyplot_env", envir = parent.env(environment()))) assign(".last_call", NULL, envir = get(".tinyplot_env", envir = parent.env(environment()))) globalVariables(c( @@ -94,5 +68,5 @@ "ylim", "ymax", "ymin" - )) + )) } diff --git a/altdoc/quarto_website.yml b/altdoc/quarto_website.yml index bc04cb4e..e5798fc0 100644 --- a/altdoc/quarto_website.yml +++ b/altdoc/quarto_website.yml @@ -19,8 +19,10 @@ website: contents: - text: Introduction file: vignettes/introduction.qmd - - text: Plot types + - text: Types file: vignettes/types.qmd + - text: Themes + file: vignettes/themes.qmd - text: Gallery file: vignettes/gallery.qmd - section: Reference @@ -31,6 +33,8 @@ website: file: man/tinyplot.qmd - text: tinyplot_add file: man/tinyplot_add.qmd + - text: tinytheme + file: man/tinytheme.qmd - text: draw_legend file: man/draw_legend.qmd - section: Plot types diff --git a/inst/tinytest/_tinysnapshot/boxplot_groups_argpass.svg b/inst/tinytest/_tinysnapshot/boxplot_groups_argpass.svg index dfb5ad38..d0487ff6 100644 --- a/inst/tinytest/_tinysnapshot/boxplot_groups_argpass.svg +++ b/inst/tinytest/_tinysnapshot/boxplot_groups_argpass.svg @@ -34,7 +34,7 @@ -Guinea Pigs' Tooth Growth +Guinea Pigs' Tooth Growth tooth length Vitamin C dose mg diff --git a/inst/tinytest/_tinysnapshot/tinytheme_basic.svg b/inst/tinytest/_tinysnapshot/tinytheme_basic.svg new file mode 100644 index 00000000..86dc734f --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_basic.svg @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + +factor(am) +0 +1 +tinytheme("basic") + + + + + + + +Title of the plot +hp +mpg + + + + + + + + + +50 +100 +150 +200 +250 +300 + + + + + + +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_bw.svg b/inst/tinytest/_tinysnapshot/tinytheme_bw.svg new file mode 100644 index 00000000..00319584 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_bw.svg @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + +factor(am) +0 +1 +tinytheme("bw") + + + + + + + +Title of the plot +hp +mpg + + + + + + + + + +50 +100 +150 +200 +250 +300 + + + + + + +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_classic.svg b/inst/tinytest/_tinysnapshot/tinytheme_classic.svg new file mode 100644 index 00000000..98b46e9f --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_classic.svg @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + +factor(am) +0 +1 +tinytheme("classic") + + + + + + + +Title of the plot +hp +mpg + + + + + + + + + +50 +100 +150 +200 +250 +300 + + + + + + +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_clean.svg b/inst/tinytest/_tinysnapshot/tinytheme_clean.svg new file mode 100644 index 00000000..d954abc5 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_clean.svg @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + +factor(am) +0 +1 +tinytheme("clean") + + + + + + + +Title of the plot +hp +mpg + + + + + + + + + +50 +100 +150 +200 +250 +300 + + + + + + +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_clean2.svg b/inst/tinytest/_tinysnapshot/tinytheme_clean2.svg new file mode 100644 index 00000000..812e04d8 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_clean2.svg @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + +factor(am) +0 +1 +tinytheme("clean2") + + + + + + + +Title of the plot +hp +mpg + + +50 +100 +150 +200 +250 +300 +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_dark.svg b/inst/tinytest/_tinysnapshot/tinytheme_dark.svg new file mode 100644 index 00000000..b884c856 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_dark.svg @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + +factor(am) +0 +1 +tinytheme("dark") + + + + + + + +Title of the plot +hp +mpg + + +50 +100 +150 +200 +250 +300 +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_default.svg b/inst/tinytest/_tinysnapshot/tinytheme_default.svg new file mode 100644 index 00000000..07e14e5c --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_default.svg @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + +factor(am) +0 +1 +tinytheme("default") + + + + + + + +Title of the plot +hp +mpg + + + + + + + + + +50 +100 +150 +200 +250 +300 + + + + + + +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_dynamic_clean.svg b/inst/tinytest/_tinysnapshot/tinytheme_dynamic_clean.svg new file mode 100644 index 00000000..1ea35617 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_dynamic_clean.svg @@ -0,0 +1,247 @@ + + + + + + + + + + + + + + + +Species +setosa +versicolor +virginica +For themes with las = 1, etc. + + + + + + + +Dynamic plot adjustment and whitespace reduction +Petal.Length +I(Sepal.Length * 1e+09) + + + + + + + + + + +1 +2 +3 +4 +5 +6 +7 + + + + + + + + + +4.5e+09 +5.0e+09 +5.5e+09 +6.0e+09 +6.5e+09 +7.0e+09 +7.5e+09 +8.0e+09 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_dynamic_clean_facet.svg b/inst/tinytest/_tinysnapshot/tinytheme_dynamic_clean_facet.svg new file mode 100644 index 00000000..7633092c --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_dynamic_clean_facet.svg @@ -0,0 +1,401 @@ + + + + + + + + + + + + + + 100 + 200 + 300 + 400 +- - +- - +- - +- - +disp +Works with facets too + + + + + + + +Dynamic plot adjustment and whitespace reduction +hp +I(mpg * 1000) + + + + + + + + + + + + + + + + +50 +100 +150 +200 +250 +300 + + + + + + +10000 +15000 +20000 +25000 +30000 + +0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +50 +100 +150 +200 +250 +300 + + + + + + +10000 +15000 +20000 +25000 +30000 + +1 + +4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +50 +100 +150 +200 +250 +300 + + + + + + +10000 +15000 +20000 +25000 +30000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +50 +100 +150 +200 +250 +300 + + + + + + +10000 +15000 +20000 +25000 +30000 + +6 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +50 +100 +150 +200 +250 +300 + + + + + + +10000 +15000 +20000 +25000 +30000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +50 +100 +150 +200 +250 +300 + + + + + + +10000 +15000 +20000 +25000 +30000 + +8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_dynamic_dark.svg b/inst/tinytest/_tinysnapshot/tinytheme_dynamic_dark.svg new file mode 100644 index 00000000..29ad0261 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_dynamic_dark.svg @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + +Species +setosa +versicolor +virginica +For themes with las = 1, etc. + + + + + + + +Dynamic plot adjustment and whitespace reduction +Petal.Length +I(Sepal.Length * 1e+09) + + +1 +2 +3 +4 +5 +6 +7 +4.5e+09 +5.0e+09 +5.5e+09 +6.0e+09 +6.5e+09 +7.0e+09 +7.5e+09 +8.0e+09 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_dynamic_dark_facet.svg b/inst/tinytest/_tinysnapshot/tinytheme_dynamic_dark_facet.svg new file mode 100644 index 00000000..d2e9aefb --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_dynamic_dark_facet.svg @@ -0,0 +1,278 @@ + + + + + + + + + + + + + + 100 + 200 + 300 + 400 +- - +- - +- - +- - +disp +Works with facets too + + + + + + + +Dynamic plot adjustment and whitespace reduction +hp +I(mpg * 1000) + + + + + + + + + +10000 +15000 +20000 +25000 +30000 + +0 + + + + + + + + + + + + + + + + + + + + + + + + + +1 + +4 + + + + + + + + + + + + + + + + + + + + + + + + +10000 +15000 +20000 +25000 +30000 + + + + + + + + + + + + + + + + + + + + + + + + + +6 + + + + + + + + + + + + + + + + + + + + + + + + +50 +100 +150 +200 +250 +300 +10000 +15000 +20000 +25000 +30000 + + + + + + + + + + + + + + + + + + + + + + + + +50 +100 +150 +200 +250 +300 + +8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_ipsum.svg b/inst/tinytest/_tinysnapshot/tinytheme_ipsum.svg new file mode 100644 index 00000000..7653f9c0 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_ipsum.svg @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + +factor(am) +0 +1 +tinytheme("ipsum") + + + + + + + +Title of the plot +hp +mpg + + +50 +100 +150 +200 +250 +300 +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_legend_bottom.svg b/inst/tinytest/_tinysnapshot/tinytheme_legend_bottom.svg new file mode 100644 index 00000000..9e86f388 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_legend_bottom.svg @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + +factor(am) +0 +1 +tinytheme("clean") + legend = "bottom!" + + + + + + + +Title of the plot +hp +mpg + + + + + + + + + +50 +100 +150 +200 +250 +300 + + + + + + +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_legend_left.svg b/inst/tinytest/_tinysnapshot/tinytheme_legend_left.svg new file mode 100644 index 00000000..a4ac5486 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_legend_left.svg @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + +factor(am) +0 +1 +tinytheme("clean") + legend = "left!" + + + + + + + +Title of the plot +hp +mpg + + + + + + + + + +50 +100 +150 +200 +250 +300 + + + + + + +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_minimal.svg b/inst/tinytest/_tinysnapshot/tinytheme_minimal.svg new file mode 100644 index 00000000..44996dc5 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_minimal.svg @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + +factor(am) +0 +1 +tinytheme("minimal") + + + + + + + +Title of the plot +hp +mpg + + +50 +100 +150 +200 +250 +300 +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_tufte.svg b/inst/tinytest/_tinysnapshot/tinytheme_tufte.svg new file mode 100644 index 00000000..0bb085a9 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_tufte.svg @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + +factor(am) +0 +1 +tinytheme("tufte") + + + + + + + +Title of the plot +hp +mpg + + + + + + + + + + + + + + + + + + +60 +80 +120 +160 +200 +240 +280 +320 + + + + + + + + + + + + + + +10 +12 +14 +16 +18 +20 +22 +24 +26 +28 +30 +32 +34 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_void.svg b/inst/tinytest/_tinysnapshot/tinytheme_void.svg new file mode 100644 index 00000000..6c451356 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_void.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + +factor(am) +0 +1 +tinytheme("void") + + + + + + + +Title of the plot +hp +mpg + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/helpers.R b/inst/tinytest/helpers.R index bba50e3b..55839986 100755 --- a/inst/tinytest/helpers.R +++ b/inst/tinytest/helpers.R @@ -8,3 +8,6 @@ library(tinysnapshot) options("tinysnapshot_os" = "Linux") options("tinysnapshot_device" = "svglite") options("tinysnapshot_device_args" = list(user_fonts = fontquiver::font_families("Liberation"))) + +# reset theme in every file +tinytheme() diff --git a/inst/tinytest/test-README.R b/inst/tinytest/test-README.R index dc6f3ef4..47001669 100644 --- a/inst/tinytest/test-README.R +++ b/inst/tinytest/test-README.R @@ -8,7 +8,7 @@ op = tpar() f = function() { tpar(mfrow = c(1, 2)) - + plot(0:10, main = "plot") tinyplot(0:10, main = "tinyplot") } @@ -16,12 +16,11 @@ expect_snapshot_plot(f, label = "readme_base_1") f = function() { tpar(mfrow = c(2, 2)) - + with(aq, plot(Day, Temp, main = "plot")) plot(Temp ~ Day, data = aq, main = "plot (formula)") with(aq, tinyplot(Day, Temp, main = "tinyplot")) tinyplot(Temp ~ Day, data = aq, main = "tinyplot (formula)") - } expect_snapshot_plot(f, label = "readme_base_2") @@ -65,9 +64,9 @@ f = function() { data = aq, type = "l", col = "black", # override automatic group colours - lty = "by" # change line type by group instead + lty = "by" # change line type by group instead ) -} +} expect_snapshot_plot(f, label = "readme_by_lty") f = function() { @@ -84,7 +83,7 @@ if ((getRversion() <= "4.3.1")) { f = function() { with( aq, - tinyplot(density(Temp), by = Month, legend = legend("topright", bty="o")) + tinyplot(density(Temp), by = Month, legend = legend("topright", bty = "o")) ) } expect_snapshot_plot(f, label = "readme_density_topright") @@ -119,7 +118,7 @@ expect_snapshot_plot(f, label = "readme_pointrange") f = function() { tpar(pch = 16, family = "HersheySans") - + tinyplot( Temp ~ Day | Month, data = aq, @@ -143,12 +142,13 @@ tpar(op) # continue with tests # -if (length(find.package("basetheme", quiet=TRUE)) == 0) exit_file("basetheme") +exit_file("basetheme: test after tinytheme if we still want to") + +if (length(find.package("basetheme", quiet = TRUE)) == 0) exit_file("basetheme") library(basetheme) basetheme("royal") # or "clean", "dark", "ink", "brutal", etc. f = function() { - tinyplot( Temp ~ Day | Month, data = aq, @@ -156,7 +156,6 @@ f = function() { palette = "Tropic", main = "Daily temperatures by month" ) - } expect_snapshot_plot(f, label = "readme_basetheme_royal") diff --git a/inst/tinytest/test-boxplot.R b/inst/tinytest/test-boxplot.R index 9219d7d6..6ae7e924 100644 --- a/inst/tinytest/test-boxplot.R +++ b/inst/tinytest/test-boxplot.R @@ -16,7 +16,8 @@ expect_snapshot_plot(f, label = "boxplot_simple_horizontal") f = function() { plt( - decrease ~ treatment, data = OrchardSprays, fill = "bisque", + decrease ~ treatment, + data = OrchardSprays, fill = "bisque", log = "y", type = "boxplot", flip = TRUE ) } @@ -26,21 +27,21 @@ expect_snapshot_plot(f, label = "boxplot_log_horizontal") ## grouped boxplots f = function() { - plt(len ~ dose | supp,data = ToothGrowth, type = "boxplot") + plt(len ~ dose | supp, data = ToothGrowth, type = "boxplot") } expect_snapshot_plot(f, label = "boxplot_groups") # fancier version with arg passing f = function() { - plt(len ~ dose | factor(supp, labels = c("Ascorbic acid", "Orange juice")), - data = ToothGrowth, - lty = 1, pch = 16, - main = "Guinea Pigs' Tooth Growth", - xlab = "Vitamin C dose mg", ylab = "tooth length", - ylim = c(0, 35), - type = type_boxplot(boxwex = 0.5, staplewex = 0), - legend = list(title = NULL), - flip = TRUE + plt(len ~ dose | factor(supp, labels = c("Ascorbic acid", "Orange juice")), + data = ToothGrowth, + lty = 1, pch = 16, + main = "Guinea Pigs' Tooth Growth", + xlab = "Vitamin C dose mg", ylab = "tooth length", + ylim = c(0, 35), + type = type_boxplot(boxwex = 0.5, staplewex = 0), + legend = list(title = NULL), + flip = TRUE ) } expect_snapshot_plot(f, label = "boxplot_groups_argpass") diff --git a/inst/tinytest/test-facet.R b/inst/tinytest/test-facet.R index c1f69d2e..f0946aa5 100644 --- a/inst/tinytest/test-facet.R +++ b/inst/tinytest/test-facet.R @@ -1,10 +1,9 @@ source("helpers.R") using("tinysnapshot") +op = par(no.readonly = TRUE) mtcars$am = as.factor(mtcars$am) -op = par() - # ## simple scatterplot cases first @@ -138,7 +137,7 @@ if (getRversion() >= "4.4.0") { f = function() { ofmar = tpar("fmar") - tpar(fmar = c(1,1,0.5,2)) + tpar(fmar = c(1, 1, 0.5, 2)) with( mtcars, tinyplot( @@ -156,7 +155,7 @@ f = function() { tinyplot( x = wt, y = mpg, facet = interaction(cyl, am), - facet.args = list(fmar = c(1,1,0.5,2)) + facet.args = list(fmar = c(1, 1, 0.5, 2)) ) ) } @@ -184,7 +183,6 @@ f = function() { expect_snapshot_plot(f, label = "facet_ribbon") f = function() { - with( mtcars1, tinyplot( @@ -241,7 +239,7 @@ if (getRversion() >= "4.4.0") { x = wt, y = mpg, by = am, facet = cyl, palette = "dark2", - grid = TRUE, #frame = FALSE, + grid = TRUE, # frame = FALSE, main = "Car efficiency", xlab = "Weight", ylab = "MPG", legend = list(title = "Transmission"), @@ -268,7 +266,7 @@ if (getRversion() >= "4.4.0") { ## Density plot versions # restore original par settings -tpar(op) +par(op) ## Avoid test fails on older R versions (pre 4.4.0) due to slight change in ## density grid value calculations. @@ -312,7 +310,7 @@ f = function() { expect_snapshot_plot(f, label = "facet_density_by") -if (getRversion() >= "4.4.0") { +if (getRversion() >= "4.4.0") { f = function() { with( mtcars, @@ -338,7 +336,8 @@ if (getRversion() >= "4.4.0") { f = function() { tinyplot( - mpg ~ wt, data = mtcars, + mpg ~ wt, + data = mtcars, facet = ~cyl ) } @@ -346,7 +345,8 @@ expect_snapshot_plot(f, label = "facet_formula") f = function() { tinyplot( - mpg ~ wt, data = mtcars, + mpg ~ wt, + data = mtcars, facet = ~am ) } @@ -354,7 +354,8 @@ expect_snapshot_plot(f, label = "facet_1x2_formula") f = function() { tinyplot( - mpg ~ wt, data = mtcars, + mpg ~ wt, + data = mtcars, facet = ~am, facet.args = list(ncol = 1) ) @@ -363,14 +364,15 @@ expect_snapshot_plot(f, label = "facet_2x1_formula") f = function() { tinyplot( - mpg ~ wt, data = mtcars, - facet = ~am:vs + mpg ~ wt, + data = mtcars, + facet = ~ am:vs ) } expect_snapshot_plot(f, label = "facet_2x2_formula") -if (getRversion() >= "4.4.0") { +if (getRversion() >= "4.4.0") { f = function() { tinyplot( ~ mpg | am, mtcars, @@ -392,7 +394,8 @@ if (getRversion() >= "4.4.0") { f = function() { tinyplot( - mpg ~ wt, data = mtcars, + mpg ~ wt, + data = mtcars, facet = am ~ cyl, main = "facet grid", sub = "Notes: Transmission (rows) vs Cylinders (cols)" @@ -402,7 +405,8 @@ expect_snapshot_plot(f, label = "facet_grid") f = function() { tinyplot( - mpg ~ wt, data = mtcars, + mpg ~ wt, + data = mtcars, facet = am ~ cyl, facet.args = list(bg = adjustcolor("hotpink", 0.5)), log = "xy", main = "facet grid (logged axes)", @@ -413,7 +417,8 @@ expect_snapshot_plot(f, label = "facet_grid_log") f = function() { tinyplot( - mpg ~ wt, data = mtcars, + mpg ~ wt, + data = mtcars, facet = am + vs ~ gear, main = "facet grid multivar", sub = "Notes: Missing combos are still displayed correctly" @@ -424,29 +429,30 @@ expect_snapshot_plot(f, label = "facet_grid_multivar") if (getRversion() >= "4.4.0") { f = function() { tinyplot( - mpg ~ wt | factor(gear), data = mtcars, + mpg ~ wt | factor(gear), + data = mtcars, facet = am ~ cyl, facet.args = list(bg = "grey90"), - pch = 19, palette = "classic", + pch = 19, palette = "classic", legend = list(title = "Gears"), main = "facet grid (fancy)", sub = "Notes: Transmission (rows) vs Cylinders (cols)", grid = TRUE, frame = FALSE, - xlim = c(1,6), ylim = c(10,35) + xlim = c(1, 6), ylim = c(10, 35) ) } expect_snapshot_plot(f, label = "facet_grid_fancy") } aq = airquality -aq$hot = ifelse(aq$Temp>=75, "hot", "cold") -aq$windy = ifelse(aq$Wind>=15, "windy", "calm") +aq$hot = ifelse(aq$Temp >= 75, "hot", "cold") +aq$windy = ifelse(aq$Wind >= 15, "windy", "calm") f = function() { tinyplot( - ~ Ozone, aq, + ~Ozone, aq, type = "density", - facet = ~hot:windy, + facet = ~ hot:windy, main = "Ozone pollution is worse on hot, calm days" ) } @@ -454,9 +460,9 @@ expect_snapshot_plot(f, label = "facet_density_formula") f = function() { tinyplot( - ~ Ozone, aq, + ~Ozone, aq, type = "density", - facet = windy ~ hot, + facet = windy ~ hot, main = "Ozone pollution is worse on hot, calm days" ) } @@ -464,7 +470,8 @@ expect_snapshot_plot(f, label = "facet_density_grid") f = function() { tinyplot( - ~ wt, data = mtcars, + ~wt, + data = mtcars, type = "hist", facet = cyl ~ am ) @@ -473,7 +480,8 @@ expect_snapshot_plot(f, label = "facet_hist_3x2") f = function() { tinyplot( - ~ wt, data = mtcars, + ~wt, + data = mtcars, type = "density", facet = cyl ~ am ) @@ -487,9 +495,9 @@ expect_snapshot_plot(f, label = "facet_density_3x2") f = function() { tinyplot( - ~ Ozone, aq, + ~Ozone, aq, type = "density", - facet = ~hot:windy, + facet = ~ hot:windy, facet.args = list(free = TRUE), main = "Free facet scales" ) @@ -498,9 +506,9 @@ expect_snapshot_plot(f, label = "facet_free") f = function() { tinyplot( - ~ Ozone, aq, + ~Ozone, aq, type = "density", - facet = windy ~ hot, + facet = windy ~ hot, facet.args = list(free = TRUE), main = "Free facet scales (grid)" ) @@ -511,4 +519,4 @@ expect_snapshot_plot(f, label = "facet_free_grid") # restore original par settings # -tpar(op) +par(op) diff --git a/inst/tinytest/test-misc.R b/inst/tinytest/test-misc.R index 4809b4ea..6da33336 100644 --- a/inst/tinytest/test-misc.R +++ b/inst/tinytest/test-misc.R @@ -83,9 +83,11 @@ if (requireNamespace("png", quietly = TRUE)) { )) obj = png::readPNG(tmp_path, info = TRUE) unlink(tmp_path) - dims = attr(obj, "dim") + # dims = attr(obj, "dim") + dims = attr(obj, "info")[["dim"]] return(dims) } - expect_equal(f(), c(1200, 1200, 4), label = "png_size") + # expect_equal(f(), c(1200, 1200, 4), label = "png_size") + expect_equal(f(), c(1200, 1200), label = "png_size") } diff --git a/inst/tinytest/test-tinytheme.R b/inst/tinytest/test-tinytheme.R new file mode 100644 index 00000000..361f660e --- /dev/null +++ b/inst/tinytest/test-tinytheme.R @@ -0,0 +1,76 @@ +source("helpers.R") +using("tinysnapshot") + +tinytheme() + + +thms = eval(formals(tinytheme)$theme) + +for (thm in thms) { + tinytheme(thm) + f = function() tinyplot( + mpg ~ hp | factor(am), data = mtcars, + main = "Title of the plot", + sub = paste0('tinytheme("', thm, '")') + ) + expect_snapshot_plot(f, label = paste0("tinytheme_", thm)) +} +rm(thm) + +# legend placement +tinytheme("clean") + +f = function() tinyplot( + mpg ~ hp | factor(am), data = mtcars, + main = "Title of the plot", + sub = 'tinytheme("clean") + legend = "left!"', + legend = "left!" +) +expect_snapshot_plot(f, label = "tinytheme_legend_left") + +f = function() tinyplot( + mpg ~ hp | factor(am), data = mtcars, + main = "Title of the plot", + sub = 'tinytheme("clean") + legend = "bottom!"', + legend = "bottom!" +) +expect_snapshot_plot(f, label = paste0("tinytheme_legend_bottom")) + +# +## Dynamic plots + +f = function() { + tinyplot( + I(Sepal.Length*1e9) ~ Petal.Length | Species, data = iris, + main = "Dynamic plot adjustment and whitespace reduction", + sub = "For themes with las = 1, etc." + ) +} + +tinytheme("clean") +f() +expect_snapshot_plot(f, label = "tinytheme_dynamic_clean") + +tinytheme("dark") +f() +expect_snapshot_plot(f, label = "tinytheme_dynamic_dark") + +f = function() { + tinyplot( + I(mpg*1e3) ~ hp | disp, data = mtcars, facet = cyl ~ am, + main = "Dynamic plot adjustment and whitespace reduction", + sub = "Works with facets too" + ) +} +tinytheme("clean") +f() +expect_snapshot_plot(f, label = "tinytheme_dynamic_clean_facet") + +tinytheme("dark") +f() +expect_snapshot_plot(f, label = "tinytheme_dynamic_dark_facet") + +# +## reset + +tinytheme() diff --git a/man/get_saved_par.Rd b/man/get_saved_par.Rd index 49b16ec5..40e911af 100644 --- a/man/get_saved_par.Rd +++ b/man/get_saved_par.Rd @@ -4,7 +4,7 @@ \alias{get_saved_par} \title{Retrieve the saved graphical parameters} \usage{ -get_saved_par(when = c("before", "after")) +get_saved_par(when = c("before", "after", "first")) } \arguments{ \item{when}{character. From when should the saved parameters be retrieved? diff --git a/man/tinyAxis.Rd b/man/tinyAxis.Rd new file mode 100644 index 00000000..f61b56d2 --- /dev/null +++ b/man/tinyAxis.Rd @@ -0,0 +1,12 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/tinyAxis.R +\name{tinyAxis} +\alias{tinyAxis} +\title{auxiliary Axis() interface with different parameter combinations based on type} +\usage{ +tinyAxis(x = NULL, ..., type = "standard") +} +\description{ +auxiliary Axis() interface with different parameter combinations based on type +} +\keyword{internal} diff --git a/man/tinytheme.Rd b/man/tinytheme.Rd new file mode 100644 index 00000000..2d921cf0 --- /dev/null +++ b/man/tinytheme.Rd @@ -0,0 +1,114 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/tinytheme.R +\name{tinytheme} +\alias{tinytheme} +\title{Set or Reset Plot Themes for \code{tinyplot}} +\usage{ +tinytheme( + theme = c("default", "basic", "clean", "clean2", "bw", "classic", "minimal", "ipsum", + "dark", "tufte", "void"), + ... +) +} +\arguments{ +\item{theme}{A character string specifying the name of the theme to apply. +Themes are arranged in an approximate hierarchy, adding or subtracting +elements in the order presented below. Note that several themes are +\emph{dynamic}, in the sense that they attempt to reduce whitespace in a way +that is responsive to the length of axes labels, tick marks, etc. These +dynamic plots are marked with an asterisk (*) below. +\itemize{ +\item \code{"default"}: inherits the user's default base graphics settings. +\item \code{"basic"}: light modification of \code{"default"}, only adding filled points, a panel background grid, and light gray background to facet titles. +\item \code{"clean"} (*): builds on \code{"basic"} by moving the subtitle above the plotting area, adding horizontal axis labels, employing tighter default plot margins and title gaps to reduce whitespace, and setting different default palettes ("Tableau 10" for discrete colors and "agSunset" for gradient colors). The first of our dynamic themes and the foundation for several derivative themes that follow below. +\item \code{"clean2"} (*): removes the plot frame (box) from \code{"clean"}, +\item \code{"classic"} (*): connects the axes in a L-shape, but removes the other top and right-hand edges of the plot frame (box). Also sets the "Okabe-Ito" palette as a default for discrete colors. Inspired by the \strong{ggplot2} theme of the same name. +\item \code{"bw"} (*): similar to \code{"clean"}, except uses thinner lines for the plot frame (box), solid grid lines, and sets the "Okabe-Ito" palette as a default for discrete colors. Inspired by the \strong{ggplot2} theme of the same name. +\item \code{"ipsum"} (*): similar to \code{"bw"}, except subtitle is italicised and axes titles are aligned to the far edges. Inspired by the \strong{hrbrthemes} theme of the same name for \strong{ggplot2}. +\item \code{"minimal"} (*): removes the plot frame (box) from \code{"bw"}, as well as the background for facet titles. Inspired by the \strong{ggplot2} theme of the same name. +\item \code{"dark"} (*): similar to \code{"minimal"}, but set against a dark background with foreground and a palette colours lightened for appropriate contrast. +\item \code{"tufte"}: floating axes and minimalist plot artifacts in the style of Edward Tufte. +\item \code{"void"}: switches off all axes, titles, legends, etc. +}} + +\item{...}{Named arguments to override specific theme settings. These +arguments are passed to \code{tpar()} and take precedence over the predefined +settings in the selected theme.} +} +\value{ +The function returns nothing. It is called for its side effects. +} +\description{ +The \code{tinytheme} function sets or resets the theme for plots created with +\code{tinyplot}. Themes control the appearance of plots, such as text alignment, +font styles, axis labels, and even dynamic margin adjustment to reduce +whitespace. +} +\details{ +Sets a list of graphical parameters using \code{tpar()} + +To reset the theme to default settings (no customization), call \code{tinytheme()} +without arguments. + +\strong{Cavear emptor:} Themes are a somewhat experimental feature of \code{tinyplot}. +We are reasonably confident that they should work as expected for most +"standard" cases. However, there may be some sharp edges. Please report any +unexpected behaviour to our GitHub repo: +https://github.com/grantmcdermott/tinyplot/issues + +Known current limitations include: +\itemize{ +\item Themes do not work well when \code{legend = "top!"}. +\item Themes do not play nicely with some complex plot types, particularly \code{"spineplot"} and \code{"ridge"}. +\item Dynamic margin spacing does not account for multi-line strings (e.g., axes +or main titles that contain \code{"\\n"}). +} +} +\examples{ + +# Reusable plot function +p = function() tinyplot( + lat ~ long | depth, data = quakes, + main = "Earthquakes off Fiji", + sub = "Data courtesy of the Harvard PRIM-H project" +) +p() + +# Set a theme +tinytheme("bw") +p() + +# Try a different theme +tinytheme("dark") +p() + +# Customize the theme by overriding default settings +tinytheme("bw", fg = "green", font.main = 2, font.sub = 3, family = "Palatino") +p() + +# Reset the theme +tinytheme() +p() + +# Themes showcase +## We'll use a slightly more intricate plot (long y-axis labs and facets) +## to demonstrate dynamic margin adjustment etc. + +thms = eval(formals(tinytheme)$theme) + +for (thm in thms) { + tinytheme(thm) + tinyplot( + I(Sepal.Length*1e4) ~ Petal.Length | Species, facet = "by", data = iris, + main = "Demonstration of tinyplot themes", + sub = paste0('tinytheme("', thm, '")') + ) +} + +# Reset +tinytheme() + +} +\seealso{ +\link{tpar} which does the heavy lifting under the hood. +} diff --git a/man/tpar.Rd b/man/tpar.Rd index 07118f50..85fbe97e 100644 --- a/man/tpar.Rd +++ b/man/tpar.Rd @@ -4,13 +4,16 @@ \alias{tpar} \title{Set or query graphical parameters} \usage{ -tpar(...) +tpar(..., hook = FALSE) } \arguments{ \item{...}{arguments of the form \code{key = value}. This includes all of the parameters typically supported by \code{\link[graphics]{par}}, as well as the \code{tinyplot}-specific ones described in the 'Graphical Parameters' section below.} + +\item{hook}{Logical. If \code{TRUE}, base graphical parameters persist across +plots via a hook applied before each new plot (see \code{?setHook}).} } \value{ When parameters are set, their previous values are returned in an @@ -56,51 +59,27 @@ you should rather use \code{par()} instead. } \section{Additional Graphical Parameters}{ - -\tabular{lll}{ -\code{facet.cex} \tab\tab Expansion factor for facet titles. Defaults to \code{1}.\cr -\tab\tab\cr -\tab\tab\cr -\code{facet.font} \tab\tab An integer corresponding to the desired font face for facet titles. For most font families and graphics devices, one of four possible values: \code{1} (regular), \code{2} (bold), \code{3} (italic), or \code{4} (bold italic). Defaults to \code{NULL}, which is equivalent to \code{1} (i.e., regular).\cr -\tab\tab\cr -\tab\tab\cr -\code{facet.col} \tab\tab Character or integer specifying the facet text colour. If an integer, will correspond to the user's default global colour palette (see \code{\link[grDevices]{palette}}). Defaults to \code{NULL}, which is equivalent to "black".\cr -\tab\tab\cr -\tab\tab\cr -\code{facet.bg} \tab\tab Character or integer specifying the facet background colour. If an integer, will correspond to the user's default colour palette (see \code{\link[grDevices]{palette}}). Passed \code{\link[graphics]{rect}}. Defaults to \code{NULL} (none).\cr -\tab\tab\cr -\tab\tab\cr -\code{facet.border} \tab\tab Character or integer specifying the facet border colour. If an integer, will correspond to the users default colour palette (see \code{\link[grDevices]{palette}}). Passed \code{\link[graphics]{rect}}. Defaults to \code{NA} (none).\cr -\tab\tab\cr -\tab\tab\cr -\code{file.height} \tab\tab Numeric specifying the height (in inches) of any plot that is written to disk using the \code{tinyplot(..., file = X)} argument. Defaults to 7.\cr -\tab\tab\cr -\tab\tab\cr -\code{file.width} \tab\tab Numeric specifying the width (in inches) of any plot that is written to disk using the \code{tinyplot(..., file = X)} argument. Defaults to 7.\cr -\tab\tab\cr -\tab\tab\cr -\code{file.res} \tab\tab Numeric specifying the resolution (in dots per square inch) of any plot that is written to disk in bitmap format (i.e., PNG or JPEG) using the \code{tinyplot(..., file = X)} argument. Defaults to 300.\cr -\tab\tab\cr -\tab\tab\cr -\code{fmar} \tab\tab A numeric vector of form \code{c(b,l,t,r)} for controlling the (base) margin padding, in terms of lines, between the individual facets in a faceted plot. Defaults to \code{c(1,1,1,1)}, i.e. a single line of padding around each facet. If more that three facets are detected, the \code{fmar} parameter is scaled by 0.75 (i.e., three-quarters) to reduce the excess whitespace that would otherwise arise due to the absent axes lines and labels. (An exception is made for 2x2 plots to better match the \code{cex} expansion logic of the base graphics system under this particular layout.) Similarly, note that an extra 0.5 lines is subtracted from each side of the facet padding for plots that aren't framed, to reduce excess whitespace.\cr -\tab\tab\cr -\tab\tab\cr -\code{grid} \tab\tab Logical indicating whether a background panel grid should be added to plots automatically. Defaults to NULL, which is equivalent to \code{FALSE}.\cr -\tab\tab\cr -\tab\tab\cr -\code{grid.col} \tab\tab Character or (integer) numeric that specifies the color of the panel grid lines. Defaults to \code{"lightgray"}.\cr -\tab\tab\cr -\tab\tab\cr -\code{grid.lty} \tab\tab Character or (integer) numeric that specifies the line type of the panel grid lines. Defaults to \code{"dotted"}.\cr -\tab\tab\cr -\tab\tab\cr -\code{grid.lwd} \tab\tab Non-negative numeric giving the line of the panel grid lines. Defaults to \code{1}.\cr -\tab\tab\cr -\tab\tab\cr -\code{lmar} \tab\tab A numeric vector of form \code{c(inner, outer)} that gives the margin padding, in terms of lines, around the automatic \code{tinyplot} legend. Defaults to \code{c(1.0, 0.1)}, where the first number represents the "inner" margin between the legend and the plot region, and the second number represents the "outer" margin between the legend and edge of the graphics device. (Note that an exception for the definition of the "outer" legend margin occurs when the legend placement is \code{"top!"}, since the legend is placed above the plot region but below the main title. In such cases, the outer margin is relative to the existing gap between the title and the plot region, which is itself determined by \code{par("mar")[3]}.)\cr -\tab\tab\cr -\tab\tab\cr -\code{ribbon.alpha} \tab\tab Numeric factor in the range \verb{[0,1]} for modifying the opacity alpha of "ribbon" and "area" (and alike) type plots. Default value is \code{0.2}.\cr +\itemize{ +\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{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). +\item \code{facet.cex}: Expansion factor for facet titles. Defaults to \code{1}. +\item \code{facet.col}: Character or integer specifying the facet text colour. If an integer, will correspond to the user's default global colour palette (see \code{palette}). Defaults to \code{NULL}, which is equivalent to "black". +\item \code{facet.font}: An integer corresponding to the desired font face for facet titles. For most font families and graphics devices, one of four possible values: \code{1} (regular), \code{2} (bold), \code{3} (italic), or \code{4} (bold italic). Defaults to \code{NULL}, which is equivalent to \code{1} (i.e., regular). +\item \code{file.height}: Numeric specifying the height (in inches) of any plot that is written to disk using the \code{tinyplot(..., file = X)} argument. Defaults to \code{7}. +\item \code{file.res}: Numeric specifying the resolution (in dots per square inch) of any plot that is written to disk in bitmap format (i.e., PNG or JPEG) using the \code{tinyplot(..., file = X)} argument. Defaults to \code{300}. +\item \code{file.width}: Numeric specifying the width (in inches) of any plot that is written to disk using the \code{tinyplot(..., file = X)} argument. Defaults to \code{7}. +\item \code{fmar}: A numeric vector of form \code{c(b,l,t,r)} for controlling the (base) margin padding, in terms of lines, between the individual facets in a faceted plot. Defaults to \code{c(1,1,1,1)}. If more than three facets are detected, the \code{fmar} parameter is scaled by 0.75 to reduce excess whitespace. For 2x2 plots, the padding better matches the \code{cex} expansion logic of base graphics. +\item \code{grid.col}: Character or (integer) numeric that specifies the color of the panel grid lines. Defaults to \code{"lightgray"}. +\item \code{grid.lty}: Character or (integer) numeric that specifies the line type of the panel grid lines. Defaults to \code{"dotted"}. +\item \code{grid.lwd}: Non-negative numeric giving the line width of the panel grid lines. Defaults to \code{1}. +\item \code{grid}: Logical indicating whether a background panel grid should be added to plots automatically. Defaults to \code{NULL}, which is equivalent to \code{FALSE}. +\item \code{lmar}: A numeric vector of form \code{c(inner, outer)} that gives the margin padding, in terms of lines, around the automatic \code{tinyplot} legend. Defaults to \code{c(1.0, 0.1)}. The inner margin is the gap between the legend and the plot region, and the outer margin is the gap between the legend and the edge of the graphics device. +\item \code{palette.qualitative}: Palette for qualitative colors. See the \code{palette} argumetn in \code{?tinyplot}. +\item \code{palette.sequential}: Palette for sequential colors. See the \code{palette} argumetn in \code{?tinyplot}. +\item \code{ribbon.alpha}: Numeric factor in the range \verb{[0,1]} for modifying the opacity alpha of "ribbon" and "area" type plots. Default value is \code{0.2}. } } @@ -114,11 +93,11 @@ tinyplot(mpg ~ wt, data = mtcars, facet = ~am) # Set params to something new. Similar to graphics::par(), note that we save # the existing values at the same time by assigning to an object. op = tpar( - las = 1, - pch = 2, - facet.bg = "grey90", - facet.cex = 2, - grid = TRUE + las = 1, + pch = 2, + facet.bg = "grey90", + facet.cex = 2, + grid = TRUE ) # Re-plot with these new params @@ -138,3 +117,10 @@ tpar(op) # options(tinyplot_grid = TRUE, tinyplot_facet.bg = "grey90") } +\seealso{ +\link[graphics:par]{graphics::par} which \code{tpar} builds on top of. \link{get_saved_par} is +a convenience function for retrieving graphical parameters at different +stages of a \code{tinyplot} call (and used for internal accounting purposes). +\link{tinytheme} allows users to easily set a group of graphics parameters +in a single function call, according to a variety of predefined themes. +} diff --git a/vignettes/introduction.qmd b/vignettes/introduction.qmd index 4f9821e5..9e245bc4 100644 --- a/vignettes/introduction.qmd +++ b/vignettes/introduction.qmd @@ -413,7 +413,8 @@ tinyplot( ``` The `facet.args` customizations can also be set globally via the `tpar()` -function. We will revisit this idea in the [Themes](#themes) section below. +function, or as part of a dedicated `tinytheme()`. We will revisit this idea in +the [Themes](#themes) section below. Finally, the `facet` argument also accepts a _two-sided_ formula for arranging facets in a fixed grid layout. Here's a simple (if contrived) example. @@ -517,66 +518,31 @@ tinyplot( ## Themes -Customizing your plots further is straightforward, whether that is done directly -by changing `tinyplot` arguments for a single plot, or by setting global -parameters. For setting global parameters, users can invoke the standard `par()` -arguments. But for improved convenience and integration with the rest of the -package, we recommend that users instead go via -[`tpar()`](https://grantmcdermott.com/tinyplot/man/tpar.html), which is an -extended version of `par()` that supports all of the latter's parameters plus -some `tinyplot`-specific ones. Here's a quick penultimate example, where we -impose several global changes (e.g., rotated axis labels, removed plot frame to -get Tufte-style floating axes, etc.) before drawing the plot. -change our point character, tick labels, and font family globally, before adding -some transparency to our colour palette, and use Tufte-style floating axes with -a background panel grid. - -```{r hershey_plus} -op = tpar( - bty = "n", # No box (frame) around the plot - family = "HersheySans", # Use R's Hershey font instead of Arial default - grid = TRUE, # Add a background grid - las = 1, # Horizontal axis tick labels - pch = 16 # Filled points as default -) - -tinyplot( - Temp ~ Day | Month, data = aq, - type = "b", - alpha = 0.5, - main = "Daily temperatures by month" -) -``` - -_Note: For access to a much wider variety of fonts, you might consider the -**showtext** package -([link](https://cran.r-project.org/web/packages/showtext/vignettes/introduction.html))._ +In the examples thus far, we have adjusted our plot aesthetics manually by +tweaking individual arguments and settings. A more convenient way to change the +look of your plots is by calling the `tinytheme()` function. This will modify +several graphical settings simultaneously to match a variety of pre-defined +styles. In addition to convenience, themes have the added benefit of enabling +dynamic adjustment of the plot regions, so that excess whitespace is reduced and +long text strings (e.g., horizontal axis labels) are accounted for. Please see +the `?tinytheme` help page, as well as the dedicated +[Themes vignette](themes.qmd) for a detailed overview of **tinyplot**'s theming +functionality. Here we provide a small taster by applying the "dark" theme to +one of our earlier plots. -At the risk of repeating ourselves, the use of `(t)par` in the previous example -again underscores the correspondence with the base graphics system. Because -`tinyplot` is effectively a convenience wrapper around base `plot`, any global -elements that you have set for the latter should carry over to the former. For -nice out-of-the-box themes, we recommend the **basetheme** package -([link](https://github.com/karoliskoncevicius/basetheme)). - -```{r basethme_royal} -tpar(op) # revert global changes from above - -library(basetheme) -basetheme("royal") # or "clean", "dark", "ink", "brutal", etc. - -tpar(pch = 15) # filled squares as first pch type +```{r} +# apply theme +tinytheme("dark") +# plot tinyplot( - Temp ~ Day | Month, data = aq, - type = "b", - pch = "by", - palette = "tropic", - main = "Daily temperatures by month" + Temp ~ Wind | Ozone, data = aq, + main = "An example of a tinytheme() in action", + sub = "Notice that the subtitle is above the plot" ) -basetheme(NULL) # back to default theme -dev.off() +# reset theme to default +tinytheme() ``` ## Saving plots diff --git a/vignettes/themes.qmd b/vignettes/themes.qmd new file mode 100644 index 00000000..cd7e99bc --- /dev/null +++ b/vignettes/themes.qmd @@ -0,0 +1,350 @@ +--- +title: "Themes" +format: html +engine: knitr +vignette: | + %\VignetteIndexEntry{Tutorial} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r include=FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>", + # fig.path = "man/figures/README-", + out.width = "70%", + fig.width = 8, + dpi = 300, + asp = 0.625 +) +``` + +The base R graphics aesthetic tends to divide option. Some people like the +default minimalist look of base R plots and/or are happy to customize the (many) +graphical parameters that are available to them. Others find base R plots ugly +and don't want to spend time endlessly tweaking different parameters. Similarly, +the inherent "canvas" approach to drawing base R graphics, with fixed placement +for plot elements, means that plots don't adjust dynamically and this can lead +to awkward whitespace artifacts unless the user explicitly accounts for them. + +Regardless of where you stand in this debate, the **tinyplot** view is that base +R graphics should ideally combine flexibility and ease of use with aesthetically +pleasing end results. One way that we enable this is via _themes_. + +## `tinytheme()` + +The `tinytheme()` function provides a mechansim for easily changing the look of +your plots to match a variety of pre-defined styles. Behind the scenes, this +works by simultaneously setting a group of graphical parameters to achieve a +particular aesthetic. Let's take a look at the "minimal" theme for example, +which is inspired by a well-known **ggplot2** theme. + +```{r} +library(tinyplot) + +tinytheme("minimal") + +tinyplot( + Sepal.Width ~ Sepal.Length | Species, + facet = "by", + data = iris, + main = "Title of the plot", + sub = "A smaller subtitle" +) +``` + +One particular feature that may interest users is the fact that `tinytheme()` +uses some internal ~magic~ logic to dynamically adjust plot margins to avoid +whitespace. For example, when long horizontal y-axis labels are detected: + +```{r} +tinyplot( + I(Sepal.Width*1e5) ~ Sepal.Length | Species, + facet = "by", + data = iris, + main = "Title of the plot", + sub = "The left-margin adjusts to accomodate the long y-axis labels" +) +``` + +As you may have noticed, the changes made by `tinytheme()` are persistent, and +apply to all subsequent `tinyplot()` calls. + +```{r} +tinyplot(mpg ~ hp, data = mtcars, main = "Fuel efficiency vs. horsepower") +``` + +To reset graphical parameters to factory defaults, call `tinytheme()` without arguments. + +```{r} +tinytheme() +tinyplot(mpg ~ hp, data = mtcars, main = "Fuel efficiency vs. horsepower") +``` + + +### Gallery + +The `tinytheme()` function ships with a number of built-in themes, which build +on top of each other in an approximately hierarchical manner (adding or +subtracting elements from the predecessor theme). + +```{r} +p = function() { + tinyplot( + Sepal.Width ~ Sepal.Length | Species, + facet = "by", + data = iris, + main = "Title of the plot", + sub = "A smaller subtitle" + ) +} +``` + + + + + + + + + + + + + + + + + + + + +#### default + +```{r} +tinytheme() ## same as tinytheme("default") +p() +``` + +#### basic + +```{r} +tinytheme("basic") +p() +``` + +#### clean + +```{r} +tinytheme("clean") +p() +``` + +#### clean2 + +```{r} +tinytheme("clean2") +p() +``` + +#### bw + +```{r} +tinytheme("bw") +p() +``` + +#### classic + +```{r} +tinytheme("classic") +p() +``` + +#### minimal + +```{r} +tinytheme("minimal") +p() +``` + +#### ipsum + +```{r} +tinytheme("ipsum") +p() +``` + +#### dark + +```{r} +tinytheme("dark") +p() +``` + +#### tufte + +```{r} +tinytheme("tufte") +p() +``` + +#### void + +```{r} +tinytheme("void") +p() +``` + + +```{r} +# Reset to default theme +tinytheme() +``` + +Please feel free to make suggestions about themes, or contribute new themes by +[opening a Pull Request on Github.](https://github.com/grantmcdermott/tinyplot) + +### Custom themes + +Tweaking existing themes is easy. For example, the `tinytheme()` function also +accepts any graphical parameter supported by `tpar()`/`par()` and applies them +in persistent fashion. + +```{r} +#| layout-ncol: 2 +tinytheme("ipsum", pch = 2, col.axis = "red", cex.main = 2, family = "Palatino") +tinyplot(mpg ~ hp, data = mtcars, main = "Fuel efficiency vs. horsepower") +tinyplot(hp ~ mpg, data = mtcars, main = "Horsepower vs. fuel efficiency") +``` + +```{r} +# reset +tinytheme() +``` + +::: {.callout-tip} +## Font families + +Fonts are a suprisingly effective way to add impact and personality to your +plots. While it is perhaps underappreciated, base R actually comes with built-in +support for quite a few font families. In the code chunk above we used the +well-known "Palatino" font, but support also extends to other popular LaTeX and +PDF fonts (see `?pdfFonts`), as well as custom fonts like the Hershey family +(see `?Hershey`). + +For access to a _much_ wider variety of fonts, you might consider the excellent +**showtext** package +([link](https://cran.r-project.org/package=showtext)). +This package allows you to install _any_ font family from the Google Font +catalog, either on-the-fly or downloaded to your permanent fontbook collection. +It plays very nicely with **tinyplot**. + +::: + +Similarly, to create your own themes "from scratch", set the theme to +`"default"` and passing additional graphical parameters to `tinytheme()`. + +```{r} +tinytheme("default", font.main = 3, col = "red") +p() +``` + +```{r} +# Reset to default theme +tinytheme() +``` + +::: {.callout-tip} +To see the full list of parameters that defines a particular theme, simply +assign them to an object. This can be helpful if you want to explore creating +your own custom theme, or tweak an existing theme. + +```{r} +#| eval: false + +# parms = tinytheme("clean") # assigns the theme at the same time +parms = tinyplot:::theme_clean # doesn't assign the theme + +# show the list of parameters used in the "clean" theme +parms +``` + +::: + +## Manual customization with `tpar()` + +_Subtitle: And comparison with `par()`_ + +Themes are a powerful and convenient way to customize your plots. But they are +not the only game in town. As any base R plotter would tell you, another way +to customize your plots by setting global graphics parameters via +[`par()`](https://search.r-project.org/R/refmans/graphics/html/par.html). +If you prefer this approach, then the good news is that it is fully compatable +with **tinyplot**.^[After all, a tinyplot is just a base plot with added +convenience features. We still use the same **graphics** engine under the hood +and any settings and workflows for `plot()` should (ideally) carry over to +`tinyplot()` too.] However, we recommend that you rather use +[`tpar()`](https://grantmcdermott.com/tinyplot/man/tpar.html), +which is an extended version of `par()` that supports all of the latter's +parameters plus some `tinyplot`-specific upgrades. + +Here is a quick example, where we impose several global changes (e.g., change +the font family, use Tufte-style floating axes with a background panel grid, +rotate tick labels, etc.) before drawing the plot. + +```{r hershey_plus} +op = tpar( + bty = "n", # No box (frame) around the plot + family = "HersheySans", # Use R's Hershey font instead of Arial default + grid = TRUE, # Add a background grid + las = 1, # Horizontal axis tick labels + pch = 19 # Larger filled points as default +) + +tinyplot(Sepal.Length ~ Petal.Length | Species, data = iris, alpha = 0.5) + +# optional: reset to the original parameter settings +tpar(op) +``` + +Again, this approach should feel very familiar to experience base R plotters. +But we will drive home the point by exploring one final difference between +vanilla `par()` and the enhanced `tpar()` equivalent... + +The graphical parameters set by `par()` stay in force as long as a graphical +device stays open. On the other hand, these parameters are reset when the +plotting window is closed or, for example, when executing a new code chunk in a +Quarto notebook.^[The **knitr** package, which provides the rendering engine for +Quarto and R Markdown, also has a `global.par` option to overcome this +limitation. See [here](https://yihui.org/knitr/options/#package-options).] + +```{r} +par(col = "red", pch = 4) +tinyplot(mpg ~ hp, data = mtcars) +``` +```{r} +tinyplot(wt ~ qsec, data = mtcars) +``` + +In contrast, graphical parameters set by `tpar()` can persist +across devices and Quarto code chunks thanks to a built-in "hook" mechanism (see +`?setHook`). To enable this persistence, we must invoke the `hook=TRUE` +argument. + +```{r} +tpar(col = "red", pch = 4, hook = TRUE) +tinyplot(mpg ~ hp, data = mtcars) +``` +```{r} +tinyplot(wt ~ qsec, data = mtcars) +``` + +```{r} +# reset defaults +tinytheme() +``` + +(Fun fact: Behind the scenes, `tinytheme()` is simply passing a list +of parameters to `tpar(..., hook = TRUE)`.)