Can i recover the aesthetic mapping from within a compute_*()
step?
#68
-
Hi everyone! This is something i've been banging my head over for weeks now. I feel confident that it's possible with limited inelegance but i've not found a solution via trial-and-error or SO (see, for example, this Q&A that almost gets me there). I hope it's an OK thing to post here, in particular before posting it to SO. The idea is to build a statistical transformation layer that uses two data sets, one that it directly transforms and another that the transformation is done with respect to. What i'd like to be able to do is transform the inherited In the example below, the referent data has only one row and the inherited data is transformed to the endpoints of the segments of their projections onto the axis of the referent data. I can make the plot i want by manually applying the aesthetic mappings, but i'd like to have this done internally. Feel free to expound on how this is a bad idea that i should not pursue, if that's how you feel. : ) For the curious, this is intended for {ordr}, as drafted for library(ggplot2)
StatProj <- ggproto(
"StatProj", Stat,
required_aes = c("x", "y"),
setup_data = function(data, params) {
# apply aesthetic mapping to data
if (is.null(params$referent[["x"]]) || is.null(params$referent[["y"]]))
stop("I want to apply an inherited aesthetic mapping, presumably here.")
data
},
compute_group = function(self, data, scales, referent = NULL, na.rm = FALSE) {
# arbitrary values of computed aesthetics
res <- transform(
data,
xend = NA_real_,
yend = NA_real_
)
# empty initialized output
res <- data[c(), , drop = FALSE]
# no referent means no projection
if (is.null(referent) || ! is.data.frame(referent)) return(res)
# compute and collect projections of `data` onto `referent` rows
inertias <- referent$x^2 + referent$y^2
for (i in seq(nrow(referent))) {
data$dots <- data$x * referent$x[i] + data$y * referent$y[i]
res_i <- transform(
data,
xend = dots / inertias[i] * referent$x[i],
yend = dots / inertias[i] * referent$y[i]
)
res <- rbind(res, res_i)
}
# output segment data
res
}
)
stat_proj <- function(
mapping = NULL, data = NULL, geom = "segment", position = "identity",
show.legend = NA,
inherit.aes = TRUE,
referent = NULL,
...
) {
layer(
data = data,
mapping = mapping,
stat = StatProj,
geom = geom,
position = position,
show.legend = show.legend,
inherit.aes = inherit.aes,
params = list(
referent = referent,
na.rm = FALSE,
...
)
)
}
# Simplify the Motor Trends data to two predictors legible at aspect ratio 1.
mtcars |>
transform(hp00 = hp/100) |>
subset(select = c(mpg, hp00, wt)) ->
subcars
# Compute the gradient of `mpg` against these two predictors.
lm(mpg ~ hp00 + wt, subcars) |>
coefficients() |>
as.list() |> as.data.frame() ->
grad
# Here's the setup; i want to project the data points onto the gradient axis.
ggplot(subcars, aes(x = hp00, y = wt)) +
coord_equal() +
geom_point() +
geom_segment(data = grad, aes(xend = 0, yend = 0)) # This doesn't work, but i want it to.
ggplot(subcars, aes(x = hp00, y = wt)) +
coord_equal() +
geom_point() +
geom_segment(data = grad, aes(xend = 0, yend = 0)) +
stat_proj(referent = grad)
#> Error in `stat_proj()`:
#> ! Problem while computing stat.
#> ℹ Error occurred in the 3rd layer.
#> Caused by error in `setup_data()`:
#> ! I want to apply an inherited aesthetic mapping, presumably here.
# This works, but i don't want it to.
ggplot(subcars, aes(x = hp00, y = wt)) +
coord_equal() +
geom_point() +
geom_segment(data = grad, aes(xend = 0, yend = 0)) +
stat_proj(referent = transform(grad, x = hp00, y = wt)) Created on 2024-12-14 with reprex v2.1.1 |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 10 replies
-
Is it important to you that this stat can be freely combined with arbitrary geoms? In other words, can we pull tricks in the |
Beta Was this translation helpful? Give feedback.
-
Interesting problem! I can't contribute to an answer, but I did want to offer my preference for this kind of reprex - use existing user-facing function when possible with new Stat, i.e. geom_segment(stat = StatProj), instead of writing your own user-facer. Just saving some typing (or search, copy, paste). Example with less interesting StatProjoutcome, compared with StatProj .... 🎈 library(tidyverse)
# Simplify the Motor Trends data to two predictors legible at aspect ratio 1.
mtcars |>
transform(hp00 = hp/100) |>
subset(select = c(mpg, hp00, wt)) ->
subcars
head(subcars)
#> mpg hp00 wt
#> Mazda RX4 21.0 1.10 2.620
#> Mazda RX4 Wag 21.0 1.10 2.875
#> Datsun 710 22.8 0.93 2.320
#> Hornet 4 Drive 21.4 1.10 3.215
#> Hornet Sportabout 18.7 1.75 3.440
#> Valiant 18.1 1.05 3.460 # Here's the setup; i want to project the data points onto the gradient axis.
ggplot(subcars) +
aes(x = hp00, y = wt) +
coord_equal() +
geom_point() compute_group_projoutcome <- function(data, scales, na.rm = FALSE) {
# arbitrary values of computed aesthetics
res <- transform(
data,
xend = NA_real_,
yend = NA_real_
)
# empty initialized output
res <- data[c(), , drop = FALSE]
lm(outcome ~ x + y, data) |>
coefficients() |>
as.list() |> as.data.frame() ->
gradient
# no referent means no projection
if (is.null(gradient) || ! is.data.frame(gradient)) return(res)
# compute and collect projections of `data` onto `referent` rows
inertias <- gradient$x^2 + gradient$y^2
for (i in seq(nrow(gradient))) {
data$dots <- data$x * gradient$x[i] + data$y * gradient$y[i]
res_i <- transform(
data,
xend = dots / inertias[i] * gradient$x[i],
yend = dots / inertias[i] * gradient$y[i]
)
res <- rbind(res, res_i)
}
res
}
subcars %>%
rename(x = hp00, y = wt, outcome = mpg) %>%
compute_group_proj() %>%
head()
#> outcome x y dots xend yend
#> Mazda RX4 21.0 1.10 2.620 -13.65494 1.726263 2.106873
#> Mazda RX4 Wag 21.0 1.10 2.875 -14.64379 1.851273 2.259445
#> Datsun 710 22.8 0.93 2.320 -11.95145 1.510907 1.844035
#> Hornet 4 Drive 21.4 1.10 3.215 -15.96225 2.017954 2.462876
#> Hornet Sportabout 18.7 1.75 3.440 -18.90000 2.389346 2.916153
#> Valiant 18.1 1.05 3.460 -16.75345 2.117978 2.584954 StatProjoutcome <- ggproto("StatProjoutcome", Stat,
required_aes = c("x", "y", "outcome"),
compute_group = compute_group_projoutcome
)
last_plot() +
geom_segment(stat = StatProjoutcome,
aes(outcome = mpg)) Created on 2024-12-14 with reprex v2.1.0 |
Beta Was this translation helpful? Give feedback.
A variant of the approach above is that you
ggproto_parent(old_layer, self)$compute_statistic()
after you've copied the computed mapping. Then you'd be unaffected by changes that ggplot2 would make inLayer$compute_statistic()
.Another approach could be to append an extra class to your layer, and use a custom
ggplot_add()
method that sort of mirrorsLayer$setup_data()
to combine global and local mappings, and pass that on as a layer parameter. This is 'sanctioned' to some degree, but a more roundabout way to getting to the same state.