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