Skip to content

Commit bc2f32c

Browse files
committed
rethink API
1 parent 5b906b9 commit bc2f32c

File tree

5 files changed

+102
-80
lines changed

5 files changed

+102
-80
lines changed

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,7 @@ export(layer_grob)
499499
export(layer_scales)
500500
export(layer_sf)
501501
export(lims)
502+
export(link_stat)
502503
export(map_data)
503504
export(margin)
504505
export(margin_auto)

R/stat-chain.R

Lines changed: 58 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,9 @@
1111
#' * A string naming the stat. To give the stat as a string, strip the
1212
#' function name of the `stat_` prefix. For example, to use `stat_count()`,
1313
#' give the stat as `"count"`.
14-
#' @param stat.params A list of parameters parallel to the `stats` argument.
15-
#' Use `NULL` elements to declare no parameters.
16-
#' @param redirect A list of mappings parallel to the `stats` argument that
17-
#' are evaluated after the stat has been computed.
14+
#' * The result of [`link_stat()`] to pass parameters or mapping instructions.
1815
#'
16+
#' @seealso [link_stat()]
1917
#' @export
2018
#'
2119
#' @examples
@@ -39,8 +37,6 @@ stat_chain <- function(
3937
position = "identity",
4038
...,
4139
stats = "identity",
42-
stat.params = list(),
43-
redirect = list(),
4440
na.rm = FALSE,
4541
show.legend = NA,
4642
inherit.aes = TRUE) {
@@ -56,60 +52,81 @@ stat_chain <- function(
5652
params = list2(
5753
na.rm = na.rm,
5854
stats = stats,
59-
stat.params = stat.params,
60-
redirect = redirect,
6155
...
6256
)
6357
)
6458
}
6559

60+
#' Parameterise a statistic computation
61+
#'
62+
#' This is a helper function for [`stat_chain()`] to pass parameters and declare
63+
#' mappings.
64+
#'
65+
#' @param stat The statistical transformation to use on the data. The `stat`
66+
#' argument accepts the following:
67+
#' * A `Stat` ggproto subclass, for example `StatCount`.
68+
#' * A string naming the stat. To give the stat as a string, strip the
69+
#' function name of the `stat_` prefix. For example, for `stat_count()`, give
70+
#' the string `"count"`.
71+
#' @param ... Other arguments passed to the stat as a parameter.
72+
#' @param mapping Set of aesthetic mappings created by [`aes()`] to be
73+
#' evaluated only after the stat has been computed.
74+
#'
75+
#' @seealso [stat_chain()]
76+
#' @returns A list bundling the stat, parameters and mapping.
77+
#' @export
78+
#'
79+
#' @examples
80+
#' # See `?stat_chain`
81+
link_stat <- function(stat, ..., mapping = aes()) {
82+
if (inherits(stat, "linked_stat")) {
83+
return(stat)
84+
}
85+
86+
stat <- validate_subclass(stat, "Stat")
87+
88+
params <- list2(...)
89+
extra <- setdiff(names(params), stat$parameters(TRUE))
90+
if (length(extra) > 0) {
91+
cli::cli_warn("Ignoring unknown parameters: {.arg {extra}}.")
92+
params <- params[setdiff(names(params), extra)]
93+
}
94+
95+
structure(
96+
list(stat = stat, params = params, mapping = validate_mapping(mapping)),
97+
class = "linked_stat"
98+
)
99+
}
100+
66101
#' @rdname ggplot2-ggproto
67102
#' @format NULL
68103
#' @usage NULL
69104
#' @export
70105
StatChain <- ggproto(
71106
"StatChain", Stat,
72107

73-
extra_params = c("na.rm", "stats", "stat.params", "redirect"),
108+
extra_params = c("na.rm", "stats"),
74109

75110
setup_params = function(data, params) {
76-
params$stats <- lapply(params$stats, validate_subclass, subclass = "Stat")
77-
n_stats <- length(params$stats)
78-
79-
params$stat.params <- force_length(
80-
params$stat.params, n_stats,
81-
warn_longer = TRUE, arg = "stat.params"
82-
)
83-
84-
params$redirect <- force_length(
85-
params$redirect, n_stats,
86-
warn_longer = TRUE, arg = "redirect"
87-
)
111+
if (inherits(params$stats, "linked_stat")) {
112+
# When a single linked stat is passed outside a list, repair to list
113+
# When using a single stat, using the appropriate `stat_*()` constructor
114+
# is better, but we should consider programmatic use too.
115+
params$stats <- list(params$stats)
116+
}
88117

118+
params$stats <- lapply(params$stats, link_stat)
89119
params
90120
},
91121

92122
compute_layer = function(self, data, params, layout) {
93123

94-
n_stats <- length(params$stats)
95-
96-
for (i in seq_len(n_stats)) {
97-
stat <- params$stats[[i]]
98-
param <- params$stat.params[[i]]
99-
100-
# We repeat the `layer()` duty of rejecting unknown parameters
101-
valid <- stat$parameters(TRUE)
102-
extra_param <- setdiff(names(param), valid)
103-
if (length(extra_param) > 0) {
104-
cli::cli_warn("Ignoring unknown parameters: {.arg {extra_param}}.")
105-
}
106-
param <- param[intersect(names(param), valid)]
107-
if (length(param) < 1) {
108-
param <- list()
109-
}
124+
for (i in seq_along(params$stats)) {
125+
link <- params$stats[[i]]
126+
stat <- link$stat
110127

111128
# Repeat `Layer$compute_statistic()` duty
112-
computed_param <- stat$setup_params(data, param)
129+
computed_param <- stat$setup_params(data, link$params)
113130
computed_param$na.rm <- computed_param$na.rm %||% params$na.rm
114131
data <- stat$setup_data(data, computed_param)
115132
data <- stat$compute_layer(data, computed_param, layout)
@@ -119,8 +136,10 @@ StatChain <- ggproto(
119136

120137
# Repeat `Layer$map_statistic()` duty, skipping backtransforms and such
121138
aes <- stat$default_aes[is_calculated_aes(stat$default_aes)]
139+
# TODO: ideally we'd have access to Layer$computed_mapping to properly
140+
# not touch user-specified mappings.
122141
aes <- aes[setdiff(names(aes), names(data))]
123-
aes <- compact(defaults(params$redirect[[i]], aes))
142+
aes <- compact(defaults(link$mapping, aes))
124143
if (length(aes) == 0) {
125144
next
126145
}
@@ -136,30 +155,3 @@ StatChain <- ggproto(
136155
data
137156
}
138157
)
139-
140-
force_length <- function(x, n = length(x), padding = list(NULL),
141-
warn_longer = FALSE, warn_shorter = FALSE,
142-
arg = caller_arg(x)) {
143-
force(arg)
144-
nx <- length(x)
145-
if (nx == n) {
146-
return(x)
147-
}
148-
n_pad <- n - nx
149-
if (n_pad > 0) {
150-
x <- c(x, rep(padding, length = n_pad))
151-
if (isTRUE(warn_shorter)) {
152-
cli::cli_warn(
153-
"Padded {.arg {arg}} with {n_pad} element{?s}."
154-
)
155-
}
156-
} else if (n_pad < 0) {
157-
x <- x[seq_len(n)]
158-
if (isTRUE(warn_longer)) {
159-
cli::cli_warn(
160-
"Dropped {abs(n_pad)} excess element{?s} from {.arg {arg}}."
161-
)
162-
}
163-
}
164-
x
165-
}

man/link_stat.Rd

Lines changed: 36 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/stat_chain.Rd

Lines changed: 4 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-stat-chain.R

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,13 @@ test_that("stat_chain can chain multiple stats", {
44

55
p <- ggplot(df, aes(x)) +
66
stat_chain(
7-
stats = "bin", stat.params = list(list(breaks = c(0.5:3.5)))
7+
stats = list(link_stat("bin", breaks = 0.5:3.5))
88
) +
99
stat_chain(
10-
stats = c("unique", "bin"),
11-
stat.params = list(NULL, list(breaks = 0.5:3.5))
10+
stats = list("unique", link_stat("bin", breaks = 0.5:3.5)),
1211
) +
1312
stat_chain(
14-
stats = c("unique", "bin"),
15-
stat.params = list(NULL, list(breaks = 0.5:3.5)),
16-
redirect = list(NULL, aes(y = -count))
13+
stats = list("unique", link_stat("bin", breaks = 0.5:3.5, mapping = aes(y = -count)))
1714
)
1815
p <- ggplot_build(p)
1916

0 commit comments

Comments
 (0)