Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extracting guide data (version 3) #5506

Merged
merged 8 commits into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ export(geom_violin)
export(geom_vline)
export(get_alt_text)
export(get_element_tree)
export(get_guide_data)
export(gg_dep)
export(ggplot)
export(ggplotGrob)
Expand Down
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# ggplot2 (development version)

* The `get_guide_data()` function can be used to extract position and label
information from the plot (#5004).

* The `trans` argument in scales and secondary axes has been renamed to
`transform`. The `trans` argument itself is deprecated. To access the
transformation from the scale, a new `get_transformation()` method is
Expand Down
86 changes: 86 additions & 0 deletions R/guides-.R
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,92 @@ Guides <- ggproto(
}
)

# Data accessor -----------------------------------------------------------

#' Extract tick information from guides
#'
#' `get_guide_data()` builds a plot and extracts information from guide keys. This
#' information typically contains positions, values and/or labels, depending
#' on which aesthetic is queried or guide is used.
#'
#' @param plot A `ggplot` or `ggplot_build` object.
#' @param aesthetic A string that describes a single aesthetic for which to
#' extract guide information. For example: `"colour"`, `"size"`, `"x"` or
#' `"y.sec"`.
#' @param i,j An integer giving a row (i) and column (j) number of a facet for
#' which to return position guide information.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this play with facets from extension packages that might not adhere to a strict grid of panels?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depends on how the facets handle the layout I suppose. From my own package, facet_manual() has .LEFT, .RIGHT, .TOP and .BOTTOM instead of ROW and COL, so it won't work.

devtools::load_all("~/packages/ggplot2")
#> ℹ Loading ggplot2
library(ggh4x)
#> 
#> Attaching package: 'ggh4x'
#> 
#> The following object is masked from 'package:ggplot2':
#> 
#>     guide_axis_logticks

ggplot(mtcars, aes(mpg, wt)) +
  geom_point() +
  facet_manual(
    ~ cyl, design = matrix(c(1,1,NA,NA,NA,2,2,NA,NA,NA,3,3), nrow = 4)
  )

get_guide_data(last_plot(), aesthetic = "x", i = 1, j = 1)
#> NULL

Created on 2023-12-14 with reprex v2.0.2

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should just allow a single index to not force a tabular layout. I know that would require some knowledge about how the facet lays out the panels but this is not something for the standard user anyway

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

basically use [[ indexing rather than [

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that idea, and match PANEL instead of ROW/COL. I think PANEL should be used in most facet extensions, I'd think.

#'
#' @return
#' One of the following:
#' * A `data.frame` representing the guide key, when the guide is unique for
#' the aesthetic.
#' * A `list` when the coord does not support position axes or multiple guides
#' match the aesthetic.
#' * `NULL` when no guide key could be found.
#' @export
#' @keywords internal
#'
#' @examples
#' # A standard plot
#' p <- ggplot(mtcars) +
#' aes(mpg, disp, colour = drat, size = drat) +
#' geom_point() +
#' facet_wrap(vars(cyl), scales = "free_x")
#'
#' # Guide information for legends
#' get_guide_data(p, "size")
#'
#' # Note that legend guides can be merged
#' merged <- p + guides(colour = "legend")
#' get_guide_data(merged, "size")
#'
#' # Guide information for positions
#' get_guide_data(p, "x", i = 1, j = 2)
#'
#' # Coord polar doesn't support proper guides, so we get a list
#' polar <- p + coord_polar()
#' get_guide_data(polar, "theta", i = 1, j = 2)
get_guide_data <- function(plot = last_plot(), aesthetic, i = 1L, j = 1L) {

check_string(aesthetic, allow_empty = FALSE)
aesthetic <- standardise_aes_names(aesthetic)

if (!inherits(plot, "ggplot_built")) {
plot <- ggplot_build(plot)
}

if (!aesthetic %in% c("x", "y", "x.sec", "y.sec", "theta", "r")) {
# Non position guides: check if aesthetic in colnames of key
keys <- lapply(plot$plot$guides$params, `[[`, "key")
keep <- vapply(keys, function(x) any(colnames(x) %in% aesthetic), logical(1))
keys <- switch(sum(keep) + 1, NULL, keys[[which(keep)]], keys[keep])
return(keys)
}

# Position guides: find the right layout entry
check_number_whole(i)
check_number_whole(j)
layout <- plot$layout$layout
select <- layout[layout$ROW == i & layout$COL == j, , drop = FALSE]
if (nrow(select) == 0) {
return(NULL)
}
params <- plot$layout$panel_params[select$PANEL][[1]]

# If panel params don't have guides, we probably have old coord system
# that doesn't use the guide system.
if (is.null(params$guides)) {
# Old system: just return relevant parameters
aesthetic <- paste(aesthetic, c("major", "minor", "labels", "range"), sep = ".")
params <- params[intersect(names(params), aesthetic)]
return(params)
} else {
# Get and return key
key <- params$guides$get_params(aesthetic)$key
return(key)
}
}

# Helpers -----------------------------------------------------------------

matched_aes <- function(layer, guide) {
Expand Down
55 changes: 55 additions & 0 deletions man/get_guide_data.Rd

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

49 changes: 49 additions & 0 deletions tests/testthat/test-guides.R
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,55 @@ test_that("guide_colourbar merging preserves both aesthetics", {
expect_true(all(c("colour", "fill") %in% names(merged$params$key)))
})

test_that("get_guide_data retrieves keys appropriately", {

p <- ggplot(mtcars, aes(mpg, disp, colour = drat, size = drat, fill = wt)) +
geom_point(shape = 21) +
facet_wrap(vars(cyl), scales = "free_x") +
guides(colour = "legend")
b <- ggplot_build(p)

# Test facetted panel
test <- get_guide_data(b, "x", i = 1, j = 2)
expect_equal(test$.label, c("18", "19", "20", "21"))

# Test plain legend
test <- get_guide_data(b, "fill")
expect_equal(test$.label, c("2", "3", "4", "5"))

# Test merged legend
test <- get_guide_data(b, "colour")
expect_true(all(c("colour", "size") %in% colnames(test)))

# Unmapped data
expect_null(get_guide_data(b, "shape"))

# Non-existent panels
expect_null(get_guide_data(b, "x", i = 2, j = 2))

expect_error(get_guide_data(b, 1), "must be a single string")
expect_error(get_guide_data(b, "x", i = "a"), "must be a whole number")
})

test_that("get_guide_data retrieves keys from exotic coords", {

p <- ggplot(mtcars, aes(mpg, disp)) + geom_point()

# Sanity check
test <- get_guide_data(p + coord_cartesian(), "x")
expect_equal(test$.label, c("10", "15", "20", "25", "30", "35"))

# We're not testing the formatting, so just testing output shape
test <- get_guide_data(p + coord_sf(crs = 3347), "y")
expect_equal(nrow(test), 5)
expect_true(all(c("x", ".value", ".label", "x") %in% colnames(test)))

# For coords that don't use guide system, we expect a list
test <- get_guide_data(p + coord_polar(), "theta")
expect_true(is.list(test) && !is.data.frame(test))
expect_equal(test$theta.labels, c("15", "20", "25", "30"))
})

test_that("guide_colourbar warns about discrete scales", {

g <- guide_colourbar()
Expand Down