Skip to content

Commit b01465d

Browse files
Expand dodge support to "p" and "l" types (#528)
* fixed.pos -> fixed.dodge * support dodge arg in points and lines types * example updates * news * test * news
1 parent 44ac114 commit b01465d

File tree

15 files changed

+439
-120
lines changed

15 files changed

+439
-120
lines changed

NEWS.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ where the formatting is also better._
66

77
## Dev version
88

9-
### Breaking change
9+
### Breaking changes
1010

1111
- "Breaking" change for internal development and custom types only:
1212
The plot settings and parameters from individual `tinyplot` calls are now
@@ -20,17 +20,24 @@ where the formatting is also better._
2020
internal changes. However, users who have defined their own custom types will
2121
need to make some adjustments to match the new `settings` logic; details are
2222
provided in the updated `Types` vignette. (#473 @vincentarelbundock and @grantmcdermott)
23+
- The ancillary `fixed.pos` argument for dodged plots has been renamed to
24+
`fixed.dodge` to avoid ambiguity, especially when passed down from a top-level
25+
`tinyplot(...)` call. (#528 @grantmcdermott)
2326

2427
### New features
2528

2629
- `type_text()` gains a `family` argument for controlling the font family,
2730
separate to the main plot text elements. (#494 @grantmcdermott)
28-
- Expanded `dodge` argument capabilities and consistency for overlapping groups:
29-
- Logical `dodge = TRUE` gives automatic width spacing based on the number
30-
of groups. (#525 @grantmcdermott)
31+
- Expanded `dodge` argument capabilities and consistency for dealing with
32+
overlapping groups:
33+
- `dodge` argument now also supported by `type_lines()`, `type_points()`, and
34+
`type_ribbon()`. (#522, #528 @grantmcdermott)
3135
- We now enforce that numeric `dodge` values must be in the range `[0,1)`.
3236
(#526 @grantmcdermott)
33-
- `dodge` argument now supported in `type_ribbon()` (#522 @grantmcdermott)
37+
- Alongside numeric values, we now support a logical `dodge = TRUE` argument,
38+
which gives automatic width spacing based on the number of groups. (#525 @grantmcdermott)
39+
- Renamed ancillary argument `fixed.pos` -> `fixed.dodge`, per the breaking
40+
change above. (#528 @grantmcdermott)
3441

3542
### Bug fixes
3643

R/dodge.R

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,25 @@
55
#'
66
#' @param datapoints Data frame containing plot data with at least `x` and `by`
77
#' columns.
8-
#' @param dodge Numeric value in the range `[0,1)`, or logical. If numeric,
9-
#' values are scaled relative to x-axis break spacing (e.g., `dodge = 0.1`
10-
#' places outermost groups one-tenth of the way to adjacent breaks;
11-
#' `dodge = 0.5` places them midway between breaks; etc.). Values < 0.5 are
12-
#' recommended. If `TRUE`, dodge width is calculated automatically based on
8+
#' @param dodge Adjustment parameter for dodging overlapping points or ranges in
9+
#' grouped plots along the x-axis (or y-axis for flipped plots). Either:
10+
#'
11+
#' - numeric value in the range `[0,1)`. Note that values are scaled
12+
#' relative to the spacing of x-axis breaks, e.g. `dodge = 0.1` places the
13+
#' outermost groups one-tenth of the way to adjacent breaks, `dodge = 0.5`
14+
#' places them midway between breaks, etc. Values < 0.5 are recommended.
15+
#' - logical. If `TRUE`, the dodge width is calculated automatically based on
1316
#' the number of groups (0.1 per group for 2-4 groups, 0.45 for 5+ groups). If
14-
#' `FALSE` or 0, no dodging is performed. Default is 0.
15-
#' @param fixed.pos Logical indicating whether dodged groups should retain a
16-
#' fixed relative position based on their group value. Relevant for `x`
17-
#' categories that only have a subset of the total number of groups. Defaults
18-
#' to `FALSE`, in which case dodging is based on the number of unique groups
19-
#' present in that `x` category alone. See Examples.
17+
#' `FALSE` or 0, no dodging is performed.
18+
#'
19+
#' Default value is 0 (no dodging). While we do not check, it is _strongly_
20+
#' recommended that dodging only be used in cases where the x-axis comprises a
21+
#' limited number of discrete breaks.
22+
#' @param fixed.dodge Logical. If `FALSE` (default), dodge positions are
23+
#' calculated independently for each `x` value, based only on the groups
24+
#' present at that position. If `TRUE`, dodge positions are based on all
25+
#' groups, ensuring "fixed" spacing across x-axis breaks (i.e., even if some
26+
#' groups are missing for a particular `x` value).
2027
#' @param cols Character vector of column names to dodge. If `NULL` (default),
2128
#' automatically detects and dodges `x`, `xmin`, and `xmax` if they exist.
2229
#' @param settings Environment containing plot settings. If `NULL` (default),
@@ -25,8 +32,8 @@
2532
#' @return Modified `datapoints` data frame with dodged positions.
2633
#'
2734
#' @details
28-
#' When `fixed.pos = TRUE`, all groups are dodged by the same amount across all
29-
#' x values, which is useful when x is categorical. When `fixed.pos = FALSE`,
35+
#' When `fixed.dodge = TRUE`, all groups are dodged by the same amount across all
36+
#' x values, which is useful when x is categorical. When `fixed.dodge = FALSE`,
3037
#' dodging is calculated independently for each x value, which is useful when
3138
#' the number of groups varies across x values.
3239
#'
@@ -37,7 +44,7 @@
3744
dodge_positions = function(
3845
datapoints,
3946
dodge,
40-
fixed.pos = TRUE,
47+
fixed.dodge = TRUE,
4148
cols = NULL,
4249
settings = NULL
4350
) {
@@ -58,7 +65,7 @@ dodge_positions = function(
5865
if (dodge >= 1) {
5966
stop("`dodge` must be in the range [0,1).", call. = FALSE)
6067
}
61-
assert_logical(fixed.pos)
68+
assert_logical(fixed.dodge)
6269

6370
if (dodge == 0) {
6471
return(datapoints)
@@ -76,7 +83,7 @@ dodge_positions = function(
7683
cols = cols[cols %in% names(datapoints)]
7784
}
7885

79-
if (fixed.pos) {
86+
if (fixed.dodge) {
8087
n = nlevels(datapoints$by)
8188
d = cumsum(rep(dodge, n))
8289
d = d - mean(d)

R/sanitize_type.R

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ sanitize_type = function(settings) {
7171
"hline" = type_hline,
7272
"j" = type_jitter,
7373
"jitter" = type_jitter,
74+
"l" = type_lines,
7475
"lines" = type_lines,
7576
"lm" = type_lm,
7677
"loess" = type_loess,
@@ -95,7 +96,7 @@ sanitize_type = function(settings) {
9596
type # default case
9697
)
9798
}
98-
99+
# browser()
99100
if (is.function(type)) {
100101
args = intersect(names(formals(type)), names(dots))
101102
args = if (length(args) >= 1L) dots[args] else list()

R/type_errorbar.R

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@
55
#' @inheritParams dodge_positions
66
#' @inheritParams graphics::arrows
77
#' @examples
8+
#' tinytheme("basic")
9+
#'
10+
#' #
11+
#' ## Basic coefficient plot(s)
12+
#'
813
#' mod = lm(mpg ~ wt * factor(am), mtcars)
914
#' coefs = data.frame(names(coef(mod)), coef(mod), confint(mod))
1015
#' colnames(coefs) = c("term", "est", "lwr", "upr")
1116
#'
12-
#' op = tpar(pch = 19)
13-
#'
1417
#' # "errorbar" and "pointrange" type convenience strings
1518
#' tinyplot(est ~ term, ymin = lwr, ymax = upr, data = coefs, type = "errorbar")
1619
#' tinyplot(est ~ term, ymin = lwr, ymax = upr, data = coefs, type = "pointrange")
@@ -19,45 +22,71 @@
1922
#' tinyplot(est ~ term, ymin = lwr, ymax = upr, data = coefs,
2023
#' type = type_errorbar(length = 0.2))
2124
#'
22-
#' # display three models side-by-side with dodging
25+
#' #
26+
#' ## Flipped plots
27+
#'
28+
#' # For flipped errobar / pointrange plots, it is recommended to use a dynamic
29+
#' # theme that applies horizontal axis tick labels
30+
#' tinyplot(est ~ term, ymin = lwr, ymax = upr, data = coefs, type = "errorbar",
31+
#' flip = TRUE, theme = "classic")
32+
#' tinyplot_add(type = 'vline', lty = 2)
33+
#'
34+
#'
35+
#' #
36+
#' ## Dodging groups
2337
#'
2438
#' models = list(
25-
#' "Model A" = lm(mpg ~ wt + cyl, data = mtcars),
26-
#' "Model B" = lm(mpg ~ wt + hp + cyl, data = mtcars),
27-
#' "Model C" = lm(mpg ~ wt, data = mtcars)
39+
#' "Model A" = lm(mpg ~ wt, data = mtcars),
40+
#' "Model B" = lm(mpg ~ wt + cyl, data = mtcars),
41+
#' "Model C" = lm(mpg ~ wt + cyl + hp, data = mtcars)
2842
#' )
2943
#'
30-
#' results = lapply(names(models), function(m) {
44+
#' models = do.call(
45+
#' rbind,
46+
#' lapply(names(models), function(m) {
3147
#' data.frame(
32-
#' model = m,
33-
#' term = names(coef(models[[m]])),
34-
#' estimate = coef(models[[m]]),
35-
#' setNames(data.frame(confint(models[[m]])), c("conf.low", "conf.high"))
48+
#' model = m,
49+
#' term = names(coef(models[[m]])),
50+
#' estimate = coef(models[[m]]),
51+
#' setNames(data.frame(confint(models[[m]])), c("conf.low", "conf.high"))
3652
#' )
37-
#' })
38-
#' results = do.call(rbind, results)
53+
#' })
54+
#' )
3955
#'
4056
#' tinyplot(estimate ~ term | model,
4157
#' ymin = conf.low, ymax = conf.high,
42-
#' data = results,
43-
#' type = type_pointrange(dodge = 0.2))
58+
#' data = models,
59+
#' type = type_pointrange(dodge = 0.1))
60+
#'
61+
#' # Aside 1: relative vs fixed dodge
62+
#' # The default dodge position is based on the unique groups (here: models)
63+
#' # available to each x value (here: coefficient term). To "fix" the dodge
64+
#' # position across all x values, use `fixed.dodge = TRUE`.
4465
#'
45-
#' # Note that the default dodged position is based solely on the number of
46-
#' # groups (here: models) available to each coefficient term. To fix the
47-
#' # position consistently across all terms, use `fixed.pos = TRUE`.
66+
#' tinyplot(estimate ~ term | model,
67+
#' ymin = conf.low, ymax = conf.high,
68+
#' data = models,
69+
#' type = type_pointrange(dodge = 0.1, fixed.dodge = TRUE))
70+
#'
71+
#' # Aside 2: layering
72+
#' # For layering on top of dodged plots, rather pass the dodging arguments
73+
#' # through the top-level call if you'd like the dodging behaviour to be
74+
#' # inherited automatically by the add layers.
4875
#'
4976
#' tinyplot(estimate ~ term | model,
5077
#' ymin = conf.low, ymax = conf.high,
51-
#' data = results,
52-
#' type = type_pointrange(dodge = 0.2, fixed.pos = TRUE))
78+
#' data = models,
79+
#' type = "pointrange",
80+
#' dodge = 0.1, fixed.dodge = TRUE)
81+
#' tinyplot_add(type = "l", lty = 2)
5382
#'
54-
#' tpar(op)
83+
#' tinytheme() # reset theme
5584
#'
5685
#' @export
57-
type_errorbar = function(length = 0.05, dodge = 0, fixed.pos = FALSE) {
86+
type_errorbar = function(length = 0.05, dodge = 0, fixed.dodge = FALSE) {
5887
out = list(
5988
draw = draw_errorbar(length = length),
60-
data = data_pointrange(dodge = dodge, fixed.pos = fixed.pos),
89+
data = data_pointrange(dodge = dodge, fixed.dodge = fixed.dodge),
6190
name = "p"
6291
)
6392
class(out) = "tinyplot_type"

R/type_lines.R

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#' @description Type function for plotting lines.
44
#'
55
#' @inheritParams graphics::plot.default
6+
#' @inheritParams dodge_positions
67
#'
78
#' @examples
89
#' # "l" type convenience character string
@@ -12,17 +13,49 @@
1213
#' tinyplot(circumference ~ age | Tree, data = Orange, type = type_lines(type = "s"))
1314
#'
1415
#' @export
15-
type_lines = function(type = "l") {
16+
type_lines = function(type = "l", dodge = 0, fixed.dodge = FALSE) {
1617
out = list(
1718
draw = draw_lines(type = type),
18-
data = NULL,
19+
data = data_lines(dodge = dodge, fixed.dodge = fixed.dodge),
1920
name = type
2021
)
2122
class(out) = "tinyplot_type"
2223
return(out)
2324
}
2425

2526

27+
data_lines = function(dodge = 0, fixed.dodge = FALSE) {
28+
if (is.null(dodge) || dodge == 0) return(NULL)
29+
fun = function(settings, ...) {
30+
env2env(settings, environment(), c("datapoints", "xlabs"))
31+
32+
if (is.character(datapoints$x)) {
33+
datapoints$x = as.factor(datapoints$x)
34+
}
35+
if (is.factor(datapoints$x)) {
36+
xlvls = unique(datapoints$x)
37+
datapoints$x = factor(datapoints$x, levels = xlvls)
38+
xlabs = seq_along(xlvls)
39+
names(xlabs) = xlvls
40+
datapoints$x = as.integer(datapoints$x)
41+
}
42+
43+
# dodge
44+
if (dodge != 0) {
45+
datapoints = dodge_positions(datapoints, dodge, fixed.dodge)
46+
}
47+
48+
x = datapoints$x
49+
env2env(environment(), settings, c(
50+
"x",
51+
"xlabs",
52+
"datapoints"
53+
))
54+
}
55+
fun
56+
}
57+
58+
2659
draw_lines = function(type = "l") {
2760
fun = function(ix, iy, icol, ipch, ibg, ilty, ilwd, icex = 1, ...) {
2861
lines(

R/type_pointrange.R

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
#' @rdname type_errorbar
22
#' @export
3-
type_pointrange = function(dodge = 0, fixed.pos = FALSE) {
3+
type_pointrange = function(dodge = 0, fixed.dodge = FALSE) {
44
out = list(
55
draw = draw_pointrange(),
6-
data = data_pointrange(dodge = dodge, fixed.pos = fixed.pos),
6+
data = data_pointrange(dodge = dodge, fixed.dodge = fixed.dodge),
77
name = "p"
88
)
99
class(out) = "tinyplot_type"
@@ -47,7 +47,7 @@ draw_pointrange = function() {
4747
}
4848

4949

50-
data_pointrange = function(dodge, fixed.pos) {
50+
data_pointrange = function(dodge, fixed.dodge) {
5151
fun = function(settings, ...) {
5252
env2env(settings, environment(), c("datapoints", "xlabs"))
5353

@@ -67,7 +67,7 @@ data_pointrange = function(dodge, fixed.pos) {
6767

6868
# dodge
6969
if (dodge != 0) {
70-
datapoints = dodge_positions(datapoints, dodge, fixed.pos)
70+
datapoints = dodge_positions(datapoints, dodge, fixed.dodge)
7171
}
7272

7373
x = datapoints$x

R/type_points.R

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#' @description Type function for plotting points, i.e. a scatter plot.
44
#' @param clim Numeric giving the lower and upper limits of the character
55
#' expansion (`cex`) normalization for bubble charts.
6+
#' @inheritParams dodge_positions
67
#'
78
#' @examples
89
#' # "p" type convenience character string
@@ -31,17 +32,17 @@
3132
#' pch = 21, fill = 0.3)
3233
#'
3334
#' @export
34-
type_points = function(clim = c(0.5, 2.5)) {
35+
type_points = function(clim = c(0.5, 2.5), dodge = 0, fixed.dodge = FALSE) {
3536
out = list(
36-
data = data_points(clim = clim),
37+
data = data_points(clim = clim, dodge = dodge, fixed.dodge = fixed.dodge),
3738
draw = draw_points(),
3839
name = "p"
3940
)
4041
class(out) = "tinyplot_type"
4142
return(out)
4243
}
4344

44-
data_points = function(clim = c(0.5, 2.5)) {
45+
data_points = function(clim = c(0.5, 2.5), dodge = 0, fixed.dodge = FALSE) {
4546
fun = function(settings, cex = NULL, ...) {
4647
env2env(settings, environment(), c("datapoints", "cex", "legend_args"))
4748

@@ -63,6 +64,11 @@ data_points = function(clim = c(0.5, 2.5)) {
6364
ylabs = NULL
6465
}
6566

67+
# dodge
68+
if (dodge != 0) {
69+
datapoints = dodge_positions(datapoints, dodge, fixed.dodge)
70+
}
71+
6672
bubble = FALSE
6773
bubble_cex = 1
6874
if (!is.null(cex) && length(cex) == nrow(datapoints)) {

0 commit comments

Comments
 (0)