From 6b43a8cb4305b8b08910a81e7ca9d563fe9073ea Mon Sep 17 00:00:00 2001 From: Teun van den Brand <49372158+teunbrand@users.noreply.github.com> Date: Fri, 3 Nov 2023 10:51:33 +0100 Subject: [PATCH 1/5] add `guide_data()` function --- R/guides-.R | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/R/guides-.R b/R/guides-.R index 76bac43de0..9d6996bda6 100644 --- a/R/guides-.R +++ b/R/guides-.R @@ -597,6 +597,92 @@ Guides <- ggproto( } ) +# Data accessor ----------------------------------------------------------- + +#' Extract tick information from guides +#' +#' `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. +#' +#' @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 +#' guide_data(p, "size") +#' +#' # Note that legend guides can be merged +#' merged <- p + guides(colour = "legend") +#' guide_data(merged, "size") +#' +#' # Guide information for positions +#' guide_data(p, "x", i = 1, j = 2) +#' +#' # Coord polar doesn't support proper guides, so we get a list +#' polar <- p + coord_polar() +#' guide_data(polar, "theta", i = 1, j = 2) +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) { From dd8b5ebe9db6491c4388654da8aededc4395a731 Mon Sep 17 00:00:00 2001 From: Teun van den Brand <49372158+teunbrand@users.noreply.github.com> Date: Fri, 3 Nov 2023 10:51:44 +0100 Subject: [PATCH 2/5] add tests --- tests/testthat/test-guides.R | 49 ++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/testthat/test-guides.R b/tests/testthat/test-guides.R index b00fe359c6..7c931df6ec 100644 --- a/tests/testthat/test-guides.R +++ b/tests/testthat/test-guides.R @@ -327,6 +327,55 @@ test_that("guide_colourbar merging preserves both aesthetics", { expect_true(all(c("colour", "fill") %in% names(merged$params$key))) }) +test_that("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 <- guide_data(b, "x", i = 1, j = 2) + expect_equal(test$.label, c("18", "19", "20", "21")) + + # Test plain legend + test <- guide_data(b, "fill") + expect_equal(test$.label, c("2", "3", "4", "5")) + + # Test merged legend + test <- guide_data(b, "colour") + expect_true(all(c("colour", "size") %in% colnames(test))) + + # Unmapped data + expect_null(guide_data(b, "shape")) + + # Non-existent panels + expect_null(guide_data(b, "x", i = 2, j = 2)) + + expect_error(guide_data(b, 1), "must be a single string") + expect_error(guide_data(b, "x", i = "a"), "must be a whole number") +}) + +test_that("guide_data retrieves keys from exotic coords", { + + p <- ggplot(mtcars, aes(mpg, disp)) + geom_point() + + # Sanity check + test <- 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 <- 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 <- 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() From 2ec8e44d4a73e128eb2ff75d3d96b8d182c4f106 Mon Sep 17 00:00:00 2001 From: Teun van den Brand <49372158+teunbrand@users.noreply.github.com> Date: Fri, 3 Nov 2023 10:52:07 +0100 Subject: [PATCH 3/5] document --- NAMESPACE | 1 + NEWS.md | 3 +++ man/guide_data.Rd | 55 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 man/guide_data.Rd diff --git a/NAMESPACE b/NAMESPACE index 717abb2e18..9c9c23877a 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -423,6 +423,7 @@ export(guide_colorbar) export(guide_colorsteps) export(guide_colourbar) export(guide_coloursteps) +export(guide_data) export(guide_gengrob) export(guide_geom) export(guide_legend) diff --git a/NEWS.md b/NEWS.md index 6bf14d4615..95a048de82 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,8 @@ # ggplot2 (development version) +* The `guide_data()` function can be used to extract position and label + information from the plot (#5004). + * `ggsave()` no longer sometimes creates new directories, which is now controlled by the new `create.dir` argument (#5489). diff --git a/man/guide_data.Rd b/man/guide_data.Rd new file mode 100644 index 0000000000..56e8354e09 --- /dev/null +++ b/man/guide_data.Rd @@ -0,0 +1,55 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guides-.R +\name{guide_data} +\alias{guide_data} +\title{Extract tick information from guides} +\usage{ +guide_data(plot = last_plot(), aesthetic, i = 1L, j = 1L) +} +\arguments{ +\item{plot}{A \code{ggplot} or \code{ggplot_build} object.} + +\item{aesthetic}{A string that describes a single aesthetic for which to +extract guide information. For example: \code{"colour"}, \code{"size"}, \code{"x"} or +\code{"y.sec"}.} + +\item{i, j}{An integer giving a row (i) and column (j) number of a facet for +which to return position guide information.} +} +\value{ +One of the following: +\itemize{ +\item A \code{data.frame} representing the guide key, when the guide is unique for +the aesthetic. +\item A \code{list} when the coord does not support position axes or multiple guides +match the aesthetic. +\item \code{NULL} when no guide key could be found. +} +} +\description{ +\code{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. +} +\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 +guide_data(p, "size") + +# Note that legend guides can be merged +merged <- p + guides(colour = "legend") +guide_data(merged, "size") + +# Guide information for positions +guide_data(p, "x", i = 1, j = 2) + +# Coord polar doesn't support proper guides, so we get a list +polar <- p + coord_polar() +guide_data(polar, "theta", i = 1, j = 2) +} +\keyword{internal} From 65e3e5fcf3d685b07d48570b197fffbb1e813e4f Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Thu, 14 Dec 2023 14:06:45 +0100 Subject: [PATCH 4/5] rename getter (see #5568) --- NAMESPACE | 2 +- NEWS.md | 3 ++- R/guides-.R | 12 ++++++------ man/{guide_data.Rd => get_guide_data.Rd} | 16 ++++++++-------- tests/testthat/test-guides.R | 24 ++++++++++++------------ 5 files changed, 29 insertions(+), 28 deletions(-) rename man/{guide_data.Rd => get_guide_data.Rd} (80%) diff --git a/NAMESPACE b/NAMESPACE index f622a7be0a..a20d03f132 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -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) @@ -439,7 +440,6 @@ export(guide_colorbar) export(guide_colorsteps) export(guide_colourbar) export(guide_coloursteps) -export(guide_data) export(guide_custom) export(guide_gengrob) export(guide_geom) diff --git a/NEWS.md b/NEWS.md index 40a3a8a869..5f11cf9415 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,7 +1,8 @@ # ggplot2 (development version) -* The `guide_data()` function can be used to extract position and label +* 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 diff --git a/R/guides-.R b/R/guides-.R index a338fd7198..d0ec2508ea 100644 --- a/R/guides-.R +++ b/R/guides-.R @@ -745,7 +745,7 @@ Guides <- ggproto( #' Extract tick information from guides #' -#' `guide_data()` builds a plot and extracts information from guide keys. This +#' `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. #' @@ -774,19 +774,19 @@ Guides <- ggproto( #' facet_wrap(vars(cyl), scales = "free_x") #' #' # Guide information for legends -#' guide_data(p, "size") +#' get_guide_data(p, "size") #' #' # Note that legend guides can be merged #' merged <- p + guides(colour = "legend") -#' guide_data(merged, "size") +#' get_guide_data(merged, "size") #' #' # Guide information for positions -#' guide_data(p, "x", i = 1, j = 2) +#' 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() -#' guide_data(polar, "theta", i = 1, j = 2) -guide_data <- function(plot = last_plot(), aesthetic, i = 1L, j = 1L) { +#' 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) diff --git a/man/guide_data.Rd b/man/get_guide_data.Rd similarity index 80% rename from man/guide_data.Rd rename to man/get_guide_data.Rd index 56e8354e09..c89b64772b 100644 --- a/man/guide_data.Rd +++ b/man/get_guide_data.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/guides-.R -\name{guide_data} -\alias{guide_data} +\name{get_guide_data} +\alias{get_guide_data} \title{Extract tick information from guides} \usage{ -guide_data(plot = last_plot(), aesthetic, i = 1L, j = 1L) +get_guide_data(plot = last_plot(), aesthetic, i = 1L, j = 1L) } \arguments{ \item{plot}{A \code{ggplot} or \code{ggplot_build} object.} @@ -27,7 +27,7 @@ match the aesthetic. } } \description{ -\code{guide_data()} builds a plot and extracts information from guide keys. This +\code{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. } @@ -39,17 +39,17 @@ p <- ggplot(mtcars) + facet_wrap(vars(cyl), scales = "free_x") # Guide information for legends -guide_data(p, "size") +get_guide_data(p, "size") # Note that legend guides can be merged merged <- p + guides(colour = "legend") -guide_data(merged, "size") +get_guide_data(merged, "size") # Guide information for positions -guide_data(p, "x", i = 1, j = 2) +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() -guide_data(polar, "theta", i = 1, j = 2) +get_guide_data(polar, "theta", i = 1, j = 2) } \keyword{internal} diff --git a/tests/testthat/test-guides.R b/tests/testthat/test-guides.R index a51776c5aa..7a06491a81 100644 --- a/tests/testthat/test-guides.R +++ b/tests/testthat/test-guides.R @@ -323,7 +323,7 @@ test_that("guide_colourbar merging preserves both aesthetics", { expect_true(all(c("colour", "fill") %in% names(merged$params$key))) }) -test_that("guide_data retrieves keys appropriately", { +test_that("get_guide_data retrieves keys appropriately", { p <- ggplot(mtcars, aes(mpg, disp, colour = drat, size = drat, fill = wt)) + geom_point(shape = 21) + @@ -332,42 +332,42 @@ test_that("guide_data retrieves keys appropriately", { b <- ggplot_build(p) # Test facetted panel - test <- guide_data(b, "x", i = 1, j = 2) + test <- get_guide_data(b, "x", i = 1, j = 2) expect_equal(test$.label, c("18", "19", "20", "21")) # Test plain legend - test <- guide_data(b, "fill") + test <- get_guide_data(b, "fill") expect_equal(test$.label, c("2", "3", "4", "5")) # Test merged legend - test <- guide_data(b, "colour") + test <- get_guide_data(b, "colour") expect_true(all(c("colour", "size") %in% colnames(test))) # Unmapped data - expect_null(guide_data(b, "shape")) + expect_null(get_guide_data(b, "shape")) # Non-existent panels - expect_null(guide_data(b, "x", i = 2, j = 2)) + expect_null(get_guide_data(b, "x", i = 2, j = 2)) - expect_error(guide_data(b, 1), "must be a single string") - expect_error(guide_data(b, "x", i = "a"), "must be a whole number") + 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("guide_data retrieves keys from exotic coords", { +test_that("get_guide_data retrieves keys from exotic coords", { p <- ggplot(mtcars, aes(mpg, disp)) + geom_point() # Sanity check - test <- guide_data(p + coord_cartesian(), "x") + 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 <- guide_data(p + coord_sf(crs = 3347), "y") + 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 <- guide_data(p + coord_polar(), "theta") + 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")) }) From f653bb6eb0f4e247269e41dc86903af43cad264f Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Thu, 14 Dec 2023 16:26:19 +0100 Subject: [PATCH 5/5] swap from location-based to panel-based --- R/guides-.R | 15 +++++++-------- man/get_guide_data.Rd | 10 +++++----- tests/testthat/test-guides.R | 6 +++--- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/R/guides-.R b/R/guides-.R index d0ec2508ea..1742d07df0 100644 --- a/R/guides-.R +++ b/R/guides-.R @@ -753,8 +753,8 @@ Guides <- ggproto( #' @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. +#' @param panel An integer giving a panel number for which to return position guide +#' information. #' #' @return #' One of the following: @@ -781,12 +781,12 @@ Guides <- ggproto( #' get_guide_data(merged, "size") #' #' # Guide information for positions -#' get_guide_data(p, "x", i = 1, j = 2) +#' get_guide_data(p, "x", panel = 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) { +#' get_guide_data(polar, "theta", panel = 2) +get_guide_data <- function(plot = last_plot(), aesthetic, panel = 1L) { check_string(aesthetic, allow_empty = FALSE) aesthetic <- standardise_aes_names(aesthetic) @@ -804,10 +804,9 @@ get_guide_data <- function(plot = last_plot(), aesthetic, i = 1L, j = 1L) { } # Position guides: find the right layout entry - check_number_whole(i) - check_number_whole(j) + check_number_whole(panel) layout <- plot$layout$layout - select <- layout[layout$ROW == i & layout$COL == j, , drop = FALSE] + select <- layout[layout$PANEL == panel, , drop = FALSE] if (nrow(select) == 0) { return(NULL) } diff --git a/man/get_guide_data.Rd b/man/get_guide_data.Rd index c89b64772b..ece14cf284 100644 --- a/man/get_guide_data.Rd +++ b/man/get_guide_data.Rd @@ -4,7 +4,7 @@ \alias{get_guide_data} \title{Extract tick information from guides} \usage{ -get_guide_data(plot = last_plot(), aesthetic, i = 1L, j = 1L) +get_guide_data(plot = last_plot(), aesthetic, panel = 1L) } \arguments{ \item{plot}{A \code{ggplot} or \code{ggplot_build} object.} @@ -13,8 +13,8 @@ get_guide_data(plot = last_plot(), aesthetic, i = 1L, j = 1L) extract guide information. For example: \code{"colour"}, \code{"size"}, \code{"x"} or \code{"y.sec"}.} -\item{i, j}{An integer giving a row (i) and column (j) number of a facet for -which to return position guide information.} +\item{panel}{An integer giving a panel number for which to return position guide +information.} } \value{ One of the following: @@ -46,10 +46,10 @@ merged <- p + guides(colour = "legend") get_guide_data(merged, "size") # Guide information for positions -get_guide_data(p, "x", i = 1, j = 2) +get_guide_data(p, "x", panel = 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(polar, "theta", panel = 2) } \keyword{internal} diff --git a/tests/testthat/test-guides.R b/tests/testthat/test-guides.R index 7a06491a81..5b672334a3 100644 --- a/tests/testthat/test-guides.R +++ b/tests/testthat/test-guides.R @@ -332,7 +332,7 @@ test_that("get_guide_data retrieves keys appropriately", { b <- ggplot_build(p) # Test facetted panel - test <- get_guide_data(b, "x", i = 1, j = 2) + test <- get_guide_data(b, "x", panel = 2) expect_equal(test$.label, c("18", "19", "20", "21")) # Test plain legend @@ -347,10 +347,10 @@ test_that("get_guide_data retrieves keys appropriately", { expect_null(get_guide_data(b, "shape")) # Non-existent panels - expect_null(get_guide_data(b, "x", i = 2, j = 2)) + expect_null(get_guide_data(b, "x", panel = 4)) 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") + expect_error(get_guide_data(b, "x", panel = "a"), "must be a whole number") }) test_that("get_guide_data retrieves keys from exotic coords", {