diff --git a/NEWS.md b/NEWS.md index 16a5430d..255fc6ff 100644 --- a/NEWS.md +++ b/NEWS.md @@ -23,10 +23,13 @@ where the formatting is also better._ ### Bug fixes -- Jittered plots now support Date/POSIXt axes. Thanks to @wachtermh for the bug - report and @vincentarelbundock for the code contribution. (#327) -- `tinyplot_add(type = "jitter")` no longer errors when layered on top of - boxplot, violin, or similar categorical plot types. (#560 @grantmcdermott) +- Several improvements/fixes to jittered plots and layering: + - Jittered plots now support Date/POSIXt axes. Thanks to @wachtermh for the + bug report and @vincentarelbundock for the code contribution. (#327) + - `tinyplot_add(type = "jitter")` no longer errors when layered on top of + boxplot, violin, or similar categorical plot types. (#560 @grantmcdermott) + - Jitter layers added via `tinyplot_add()` now align correctly with grouped + (offset) boxplot, violin, and ridge base layers. (#493 @grantmcdermott) ### Internals diff --git a/R/tinyplot.R b/R/tinyplot.R index 71e125d1..2ba8f847 100644 --- a/R/tinyplot.R +++ b/R/tinyplot.R @@ -795,10 +795,13 @@ tinyplot.default = function( ribbon.alpha = sanitize_ribbon_alpha(NULL), # misc - flip = flip, - dodge = NULL, + add = add, by = by, + dodge = NULL, dots = dots, + flip = flip, + group_offsets = NULL, + offsets_axis = NULL, type_info = list() # pass type-specific info from type_data to type_draw ) @@ -873,6 +876,8 @@ tinyplot.default = function( # ensure axis aligment of any added layers if (!add) { assign("xlabs_orig", settings[["xlabs"]], envir = get(".tinyplot_env", envir = parent.env(environment()))) + assign(".group_offsets", settings[["group_offsets"]], envir = get(".tinyplot_env", envir = parent.env(environment()))) + assign(".offsets_axis", settings[["offsets_axis"]], envir = get(".tinyplot_env", envir = parent.env(environment()))) } else { align_layer(settings) } diff --git a/R/type_boxplot.R b/R/type_boxplot.R index d46fe330..6610846c 100644 --- a/R/type_boxplot.R +++ b/R/type_boxplot.R @@ -43,7 +43,7 @@ type_boxplot = function( boxwex = boxwex, staplewex = staplewex, outwex = outwex), - data = data_boxplot(), + data = data_boxplot(boxwex = boxwex), name = "boxplot" ) class(out) = "tinyplot_type" @@ -60,13 +60,9 @@ draw_boxplot = function(range, width, varwidth, notch, outline, boxwex, staplewe # Handle multiple groups if (ngrps > 1 && isFALSE(x_by) && isFALSE(facet_by)) { - boxwex_orig = boxwex + group_offsets = get_environment_variable(".group_offsets") boxwex = boxwex / ngrps - 0.01 - at_ix = at_ix + seq( - -((boxwex_orig - boxwex) / 2), - ((boxwex_orig - boxwex) / 2), - length.out = ngrps - )[iby] + at_ix = at_ix + group_offsets[iby] } boxplot( @@ -93,7 +89,7 @@ draw_boxplot = function(range, width, varwidth, notch, outline, boxwex, staplewe -data_boxplot = function() { +data_boxplot = function(boxwex = 0.8) { fun = function(settings, ...) { env2env(settings, environment(), c("datapoints", "by", "facet", "null_facet", "null_palette", "x", "col", "bg", "null_by")) # Convert x to factor if it's not already @@ -134,6 +130,20 @@ data_boxplot = function() { by = if (length(unique(datapoints$by)) > 1) datapoints$by else by facet = if (length(unique(datapoints$facet)) > 1) datapoints$facet else facet + # Compute group offsets for multi-group boxplots + ngrps = length(unique(datapoints$by)) + if (ngrps > 1 && !settings$x_by) { + boxwex_grp = boxwex / ngrps - 0.01 + group_offsets = seq( + -((boxwex - boxwex_grp) / 2), + ((boxwex - boxwex_grp) / 2), + length.out = ngrps + ) + } else { + group_offsets = rep(0, max(ngrps, 1)) + } + offsets_axis = "x" + # legend customizations settings$legend_args[["pch"]] = settings$legend_args[["pch"]] %||% 22 settings$legend_args[["pt.cex"]] = settings$legend_args[["pt.cex"]] %||% 3.5 @@ -150,11 +160,10 @@ data_boxplot = function() { "col", "bg", "by", - "facet")) + "facet", + "group_offsets", + "offsets_axis" + )) } return(fun) } - - - - diff --git a/R/type_jitter.R b/R/type_jitter.R index 6a2ea6f1..21363f0d 100644 --- a/R/type_jitter.R +++ b/R/type_jitter.R @@ -40,7 +40,7 @@ jitter_restore = function(obj, factor, amount) { data_jitter = function(factor, amount) { fun = function(settings, ...) { - env2env(settings, environment(), "datapoints") + env2env(settings, environment(), c("datapoints", "add")) x = datapoints$x y = datapoints$y @@ -60,6 +60,35 @@ data_jitter = function(factor, amount) { } else { ylabs = NULL } + + # Apply group offsets from base layer (e.g., boxplot, violin, ridge) + group_offsets = get_environment_variable(".group_offsets") + offsets_axis = get_environment_variable(".offsets_axis") + if (isTRUE(add) && !is.null(group_offsets)) { + if (identical(offsets_axis, "x") && is.factor(datapoints$by)) { + # x-axis offsets (boxplot, violin): keyed by group level + if (is.null(xlabs)) { + xf = as.factor(x) + xlvls = levels(xf) + xlabs = seq_along(xlvls) + names(xlabs) = xlvls + x = as.integer(xf) + } + x = x + group_offsets[as.integer(datapoints$by)] + } else if (identical(offsets_axis, "y")) { + # y-axis offsets (ridge): keyed by y-level name + if (is.null(ylabs)) { + yf = as.factor(y) + ylvls = levels(yf) + ylabs = seq_along(ylvls) + names(ylabs) = ylvls + y = as.integer(yf) + } + y_labels = names(ylabs)[y] + y = group_offsets[y_labels] + } + } + x = jitter_restore(x, factor = factor, amount = amount) y = jitter_restore(y, factor = factor, amount = amount) diff --git a/R/type_ridge.R b/R/type_ridge.R index 24133f7f..605a0c40 100644 --- a/R/type_ridge.R +++ b/R/type_ridge.R @@ -332,6 +332,11 @@ data_ridge = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512, } datapoints = do.call(rbind, lapply(datapoints, offset_z)) + # Store y-level offsets for added layers (e.g., jitter) + ylevs = unique(datapoints$y) + group_offsets = setNames(seq_along(ylevs) - 1L, ylevs) + offsets_axis = "y" + if (y_by) { datapoints$y = factor(datapoints$y) datapoints$by = factor(datapoints$y, levels = rev(levels(datapoints$y))) @@ -420,7 +425,9 @@ data_ridge = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512, "datapoints", "yaxt", "ylim", - "type_info" + "type_info", + "group_offsets", + "offsets_axis" )) } return(fun) diff --git a/R/type_violin.R b/R/type_violin.R index f1d7969d..42eb2dd5 100644 --- a/R/type_violin.R +++ b/R/type_violin.R @@ -151,6 +151,19 @@ data_violin = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512, } } + # Compute group offsets for multi-group violins + if (ngrps > 1 && isFALSE(x_by) && isFALSE(facet_by)) { + xwidth_grp = width / ngrps - 0.01 + group_offsets = seq( + -((width - xwidth_grp) / 2), + ((width - xwidth_grp) / 2), + length.out = ngrps + ) + } else { + group_offsets = rep(0, max(ngrps, 1)) + } + offsets_axis = "x" + datapoints = lapply(seq_along(datapoints), function(d) { dat = datapoints[[d]] if (trim) { @@ -182,7 +195,7 @@ data_violin = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512, xwidth = xwidth_orig / ngrps - 0.01 x = rescale_num(x, to = c(0, xwidth)) x = x + as.numeric(sub("^([0-9]+)\\..*", "\\1", names(datapoints)[d])) - xwidth/2 - x = x + seq(-((xwidth_orig - xwidth) / 2), ((xwidth_orig - xwidth) / 2), length.out = ngrps)[dat$by[1]] + x = x + group_offsets[dat$by[1]] } else if (nfacets > 1) { x = rescale_num(x, to = c(0, xwidth)) x = x + as.numeric(sub("^([0-9]+)\\..*", "\\1", names(datapoints)[d])) - xwidth/2 @@ -221,9 +234,10 @@ data_violin = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512, "ylab", "xlabs", "col", - "bg" + "bg", + "group_offsets", + "offsets_axis" )) } return(fun) } - diff --git a/inst/tinytest/_tinysnapshot/tinyplot_add_jitter_on_grouped_boxplot.svg b/inst/tinytest/_tinysnapshot/tinyplot_add_jitter_on_grouped_boxplot.svg new file mode 100644 index 00000000..0a0032ed --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinyplot_add_jitter_on_grouped_boxplot.svg @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + +supp +OJ +VC + + + + + + + +dose +len + + + + + + +0.5 +1 +2 + + + + + + + + +5 +10 +15 +20 +25 +30 +35 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinyplot_add_jitter_on_grouped_violin.svg b/inst/tinytest/_tinysnapshot/tinyplot_add_jitter_on_grouped_violin.svg new file mode 100644 index 00000000..4a8e977e --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinyplot_add_jitter_on_grouped_violin.svg @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + +supp +OJ +VC + + + + + + + +dose +len + + + + + + +0.5 +1 +2 + + + + + + +0 +10 +20 +30 +40 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/test-tinyplot_add.R b/inst/tinytest/test-tinyplot_add.R index 9b72ddac..1f5914ef 100644 --- a/inst/tinytest/test-tinyplot_add.R +++ b/inst/tinytest/test-tinyplot_add.R @@ -95,10 +95,26 @@ f = function() { expect_snapshot_plot(f, label = "tinyplot_add_no_recursive_margins") -# jitter layer on top of boxplot (#559) -set.seed(42) +# jitter layer on top of violin (#559) f = function() { + set.seed(42) tinyplot(Sepal.Length ~ Species, data = iris, type = "violin") tinyplot_add(type = "jitter", cex = 0.5, alpha = 0.3) } expect_snapshot_plot(f, label = "tinyplot_add_jitter_on_violin") + +# jitter layer on top of grouped boxplot (#493) +f = function() { + set.seed(42) + tinyplot(len ~ dose | supp, data = ToothGrowth, type = "boxplot") + tinyplot_add(type = "jitter", cex = 0.5, alpha = 0.3) +} +expect_snapshot_plot(f, label = "tinyplot_add_jitter_on_grouped_boxplot") + +# jitter layer on top of grouped violin (#493) +f = function() { + set.seed(42) + tinyplot(len ~ dose | supp, data = ToothGrowth, type = "violin", bg = 0.2) + tinyplot_add(type = "jitter", cex = 0.5, alpha = 0.3) +} +expect_snapshot_plot(f, label = "tinyplot_add_jitter_on_grouped_violin")