Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ where the formatting is also better._

- `type_text()` gains a `family` argument for controlling the font family,
separate to the main plot text elements. (#494 @grantmcdermott)
- `type_ribbon()` gains a `dodge` argument, supporting similar functionality to
`type_errorbar()` and `type_pointrange()` for dodging overlapping groups.
(#522 @grantmcdermott)

### Bug fixes

Expand Down
66 changes: 66 additions & 0 deletions R/dodge.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#' Dodge positions for grouped data
#'
#' Adjusts x-coordinates (and optionally xmin/xmax) to dodge overlapping points
#' or ranges in grouped plots.
#'
#' @param datapoints Data frame containing plot data with at least `x` and `by`
#' columns.
#' @param dodge Numeric value specifying the dodge amount. If 0, no dodging is
#' performed.
#' @param fixed.pos Logical. If `TRUE`, dodge positions are fixed based on the
#' number of groups in `by`. If `FALSE`, dodge positions are calculated
#' separately for each unique x value.
#' @param cols Character vector of column names to dodge. If `NULL` (default),
#' automatically detects and dodges `x`, `xmin`, and `xmax` if they exist.
#'
#' @return Modified `datapoints` data frame with dodged positions.
#'
#' @details
#' When `fixed.pos = TRUE`, all groups are dodged by the same amount across all
#' x values, which is useful when x is categorical. When `fixed.pos = FALSE`,
#' dodging is calculated independently for each x value, which is useful when
#' the number of groups varies across x values.
#'
#' If `cols` is not specified, the function automatically dodges `x` and any
#' `xmin`/`xmax` columns that exist in the data.
#'
#' @keywords internal
dodge_positions = function(
datapoints,
dodge,
fixed.pos = TRUE,
cols = NULL
) {
if (dodge == 0) {
return(datapoints)
}

# Auto-detect columns to dodge if not specified
if (is.null(cols)) {
cols = c("x", "xmin", "xmax")
cols = cols[cols %in% names(datapoints)]
}

if (fixed.pos) {
n = nlevels(datapoints$by)
d = cumsum(rep(dodge, n))
d = d - mean(d)
x_adj = d[as.integer(datapoints$by)]
for (col in cols) {
datapoints[[col]] = datapoints[[col]] + x_adj
}
} else {
xuniq = unique(datapoints$x)
for (i in seq_along(xuniq)) {
idx = which(datapoints$x == xuniq[i])
n = length(idx)
d = cumsum(rep(dodge, n))
d = d - mean(d)
for (col in cols) {
datapoints[[col]][idx] = datapoints[[col]][idx] + d
}
}
}

datapoints
}
21 changes: 1 addition & 20 deletions R/type_pointrange.R
Original file line number Diff line number Diff line change
Expand Up @@ -70,26 +70,7 @@ data_pointrange = function(dodge, fixed.pos) {

# dodge
if (dodge != 0) {
if (fixed.pos) {
n = nlevels(datapoints$by)
d = cumsum(rep(dodge, n))
d = d - mean(d)
x_adj = d[as.integer(datapoints$by)]
datapoints$x = datapoints$x + x_adj
datapoints$xmin = datapoints$xmin + x_adj
datapoints$xmax = datapoints$xmax + x_adj
} else {
xuniq = unique(datapoints$x)
for (i in seq_along(xuniq)) {
idx = which(datapoints$x == xuniq[i])
n = length(idx)
d = cumsum(rep(dodge, n))
d = d - mean(d)
datapoints$x[idx] = datapoints$x[idx] + d
datapoints$xmin[idx] = datapoints$xmin[idx] + d
datapoints$xmax[idx] = datapoints$xmax[idx] + d
}
}
datapoints = dodge_positions(datapoints, dodge, fixed.pos)
}

x = datapoints$x
Expand Down
54 changes: 51 additions & 3 deletions R/type_ribbon.R
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,20 @@
#' If no `alpha` value is provided, then will default to `tpar("ribbon.alpha")`
#' (i.e., probably `0.2` unless this has been overridden by the user in their global
#' settings.)
#' @inheritParams type_errorbar
#'
#' @description Type constructor functions for producing polygon ribbons, which
#' define a `y` interval (usually spanning from `ymin` to `ymax`) for each
#' `x` value. Area plots are a special case of ribbon plot where `ymin` is
#' set to 0 and `ymax` is set to `y`.
#'
#' @section Dodging ribbon plots:
#'
#' We support dodging for grouped ribbon plots, enabling similar functionality
#' to dodged errorbar and pointrange plots. However, it is strongly recommended
#' that dodging is only implemented for cases where the x-axis comprises a
#' limited number of discrete cases (e.g., coefficient or event-study plots).
#' See Examples.
#'
#' @examples
#' x = 1:100 / 10
Expand Down Expand Up @@ -37,11 +46,45 @@
#'
#' # Area plots are often used for time series charts
#' tinyplot(AirPassengers, type = "area")
#'
#' #
#' ## Dodged ribbon/area plots
#'
#' # Dodged ribbon or area plots can be useful in cases where there is strong
#' # overlap across groups (and a limited number of discrete x-axis values).
#'
#' dat = data.frame(
#' x = rep(c("Before", "After"), each = 2),
#' grp = rep(c("A", "B"), 2),
#' y = c(10, 10.5, 15, 15.3),
#' lwr = c(8, 8.5, 13, 13.3),
#' upr = c(12, 12.5, 17, 17.3)
#' )
#'
#' tinyplot(
#' y ~ x | grp,
#' data = dat,
#' ymin = lwr, ymax = upr,
#' type = type_ribbon(),
#' main = "Overlappling ribbons"
#' )
#'
#' tinyplot(
#' y ~ x | grp,
#' data = dat,
#' ymin = lwr, ymax = upr,
#' type = type_ribbon(dodge = 0.1),
#' main = "Dodged ribbons"
#' )
#'
#' @export
type_ribbon = function(alpha = NULL) {
type_ribbon = function(alpha = NULL, dodge = 0, fixed.pos = FALSE) {
assert_numeric(dodge, len = 1, lower = 0)
assert_logical(fixed.pos)

out = list(
draw = draw_ribbon(),
data = data_ribbon(ribbon.alpha = alpha),
data = data_ribbon(ribbon.alpha = alpha, dodge = dodge, fixed.pos = fixed.pos),
name = "ribbon"
)
class(out) = "tinyplot_type"
Expand All @@ -64,7 +107,7 @@ draw_ribbon = function() {
}


data_ribbon = function(ribbon.alpha = NULL) {
data_ribbon = function(ribbon.alpha = NULL, dodge = 0, fixed.pos = FALSE) {
ribbon.alpha = sanitize_ribbon_alpha(ribbon.alpha)
fun = function(settings, ...) {
env2env(settings, environment(), c("datapoints", "xlabs", "null_by", "null_facet"))
Expand All @@ -81,6 +124,11 @@ data_ribbon = function(ribbon.alpha = NULL) {
} else {
xlabs = NULL
}

# dodge (auto-detects x, xmin, xmax columns)
if (dodge != 0) {
datapoints = dodge_positions(datapoints, dodge, fixed.pos)
}

if (null_by && null_facet) {
xord = order(datapoints$x)
Expand Down
77 changes: 77 additions & 0 deletions inst/tinytest/_tinysnapshot/ribbon_dodge.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions inst/tinytest/test-type_ribbon.R
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,22 @@ f = function() {
)
}
expect_snapshot_plot(f, label = "ribbon_alpha50")


# Dodged ribbon test
f = function() {
dat = data.frame(
x = rep(c("Before", "After"), each = 2),
grp = rep(c("A", "B"), 2),
y = c(10, 10.5, 15, 15.3),
lwr = c(8, 8.5, 13, 13.3),
upr = c(12, 12.5, 17, 17.3)
)
tinyplot(
y ~ x | grp,
data = dat,
ymin = lwr, ymax = upr,
type = type_ribbon(dodge = 0.1)
)
}
expect_snapshot_plot(f, label = "ribbon_dodge")
39 changes: 39 additions & 0 deletions man/dodge_positions.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading