teunbrand / ggh4x

ggplot extension: options for tailored facets, multiple colourscales and miscellaneous
https://teunbrand.github.io/ggh4x/
Other
542 stars 33 forks source link

Programmatically constructing a list of formulas to pass to `facetted_pos_scales` #52

Closed yjunechoe closed 2 years ago

yjunechoe commented 2 years ago

Thanks for this amazing package!

My plot involves creating many facets and I'm trying to pass in a different scale for each facet using facetted_pos_scales. I'd like the scales to vary programmatically between facets, so I tried constructing a list of formulas using purrr::map and then passing it to facetted_pos_scales. Surprisingly this failed the check in ggh4x:::check_facetted_scale, even though other ways of indirectly providing the list of formulas work (so it doesn't seem like an issue with facetted_pos_scales quoting arguments?).

Perhaps this is just my lack of knowledge in metaprogramming but thanks in advance!

library(ggh4x)

# Adapted from https://teunbrand.github.io/ggh4x/reference/facetted_pos_scales.html

plot <- ggplot(iris, aes(Sepal.Width, Sepal.Length)) +
  geom_point(aes(colour = Species)) +
  facet_wrap(Species ~ ., scales = "free_y")

# the typed-out way demoed in the docs
plot +
  facetted_pos_scales(
    y = list(
      Species == "virginica" ~ scale_y_continuous(
        guide = guide_axis_manual(labels = "virginica")
      ),
      Species == "versicolor" ~ scale_y_continuous(
        guide = guide_axis_manual(labels = "versicolor")
      )
    )
  )

# saving list of formula to a variable first and then passing that variable works
scale_specs <- list(
  Species == "virginica" ~ scale_y_continuous(
    guide = guide_axis_manual(labels = "virginica")
  ),
  Species == "versicolor" ~ scale_y_continuous(
    guide = guide_axis_manual(labels = "versicolor")
  )
)

plot +
  facetted_pos_scales(
    y = scale_specs
  )

library(rlang)

# programmatically constructed list of formulas does not work

scale_specs_programmatic <- purrr::map(c("virginica", "versicolor"), ~ {
  expr(Species == !!.x ~ scale_y_continuous(
    guide = guide_axis_manual(labels = !!.x)
  ))
})

plot +
  facetted_pos_scales(
    y = scale_specs_programmatic
  )
#> Error in facetted_pos_scales(y = scale_specs_programmatic): Invalid facetted scale specifications.

# test for equality all return true
scale_specs
#> [[1]]
#> Species == "virginica" ~ scale_y_continuous(guide = guide_axis_manual(labels = "virginica"))
#> 
#> [[2]]
#> Species == "versicolor" ~ scale_y_continuous(guide = guide_axis_manual(labels = "versicolor"))
scale_specs_programmatic
#> [[1]]
#> Species == "virginica" ~ scale_y_continuous(guide = guide_axis_manual(labels = "virginica"))
#> 
#> [[2]]
#> Species == "versicolor" ~ scale_y_continuous(guide = guide_axis_manual(labels = "versicolor"))
scale_specs[[1]] == scale_specs_programmatic[[1]]
#> [1] TRUE
all.equal(scale_specs, scale_specs_programmatic)
#> [1] TRUE

Created on 2021-09-26 by the reprex package (v2.0.1)

teunbrand commented 2 years ago

Hi June,

I'm not fluent in metaprogramming either, but it seems the only piece of the puzzle you might be missing is as.formula(). The purrr::map() operation you were doing was giving a list of calls instead of a list of formulas, as far as I can tell.

library(ggh4x)
#> Loading required package: ggplot2
library(rlang)

plot <- ggplot(iris, aes(Sepal.Width, Sepal.Length)) +
  geom_point(aes(colour = Species)) +
  facet_wrap(Species ~ ., scales = "free_y")

# Coerce with as formula
scale_specs_programmatic <- purrr::map(c("virginica", "versicolor"), ~ {
  as.formula(expr(Species == !!.x ~ scale_y_continuous(
    guide = guide_axis_manual(labels = !!.x)
  )))
})

plot +
  facetted_pos_scales(
    y = scale_specs_programmatic
  )

Created on 2021-09-26 by the reprex package (v2.0.1)

As a sidenote, I don't think all.equal() is the best way to check for equality as it ignores attributes such as classes.

all.equal(scale_specs, scale_specs_programmatic)
#> [1] TRUE
identical(scale_specs, scale_specs_programmatic)
#> [1] FALSE
waldo::compare(scale_specs, scale_specs_programmatic)
#> `old[[1]]` is an S3 object of class <formula>, a call
#> `new[[1]]` is a call
#> 
#> `old[[2]]` is an S3 object of class <formula>, a call
#> `new[[2]]` is a call

Best wishes, Teun

yjunechoe commented 2 years ago

Beautiful! Wrapping the expression in as.formula does indeed do the trick. Thanks for the quick solution!