From f5e354a4230d033765035a5757058097a51ced17 Mon Sep 17 00:00:00 2001 From: phgrosjean Date: Tue, 23 Aug 2022 19:51:18 +0200 Subject: [PATCH] Add gg() --- DESCRIPTION | 4 +- NAMESPACE | 5 ++ NEWS.md | 6 ++- R/chart-package.R | 2 +- R/gg.R | 133 ++++++++++++++++++++++++++++++++++++++++++++++ README.md | 2 +- TODO.md | 2 + inst/WORDLIST | 2 + man/gg.Rd | 63 ++++++++++++++++++++++ 9 files changed, 214 insertions(+), 5 deletions(-) create mode 100644 R/gg.R create mode 100644 man/gg.Rd diff --git a/DESCRIPTION b/DESCRIPTION index e53652f..1606cba 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: chart Type: Package -Version: 1.3.1 +Version: 1.4.0 Title: General Charting (Plotting) Function Description: Chart generalizes plot generation in R, being with base R plot function, lattice or ggplot2. A formula interface is available for both. @@ -8,7 +8,7 @@ Authors@R: c(person("Philippe", "Grosjean", role = c("aut", "cre"), email = "phgrosjean@sciviews.org", comment = c(ORCID = "0000-0002-2694-9471"))) Maintainer: Philippe Grosjean -Depends: R (>= 3.3.0), lattice, ggplot2 (>= 3.0) +Depends: R (>= 4.0.0), lattice, ggplot2 (>= 3.0) Imports: stats, utils, rlang, cowplot, grDevices, graphics, scales, latticeExtra, viridis, pryr, data.io, ggplotify, ggpubr Suggests: MASS, covr, knitr, testthat, rmarkdown, spelling Remotes: SciViews/data.io diff --git a/NAMESPACE b/NAMESPACE index 86effab..b08f18c 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,12 +1,15 @@ # Generated by roxygen2: do not edit by hand +S3method("$",subsettable_gg) S3method("$",subsettable_type) +S3method(.DollarNames,subsettable_gg) S3method(chart,"function") S3method(chart,default) export(chart) export(chart_theme) export(combine_charts) export(f_aes) +export(gg) export(ggarrange) export(theme_sciviews) export(theme_sciviews_graphics) @@ -40,5 +43,7 @@ importFrom(scales,gradient_n_pal) importFrom(scales,hue_pal) importFrom(stats,as.formula) importFrom(stats,asOneSidedFormula) +importFrom(utils,.DollarNames) +importFrom(utils,apropos) importFrom(utils,modifyList) importFrom(viridis,viridis_pal) diff --git a/NEWS.md b/NEWS.md index 4751870..2c56f38 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,8 +1,12 @@ +# chart 1.4.0 + +- The `gg()` subsettable function allows to use the pipe instead of the `+` in ggplot2, like `ggplot() |> gg$geom_point()`. Also, a completion list is available when typing `gg$`. + # chart 1.3.1 - Code in `chart.default()` choked `R CMD Check` (variable `y` not defined in scope) is commented out. Expect a slightly different behavior with automatic `geom_point()` with only one variable. -- GitHub repository cleaned up (spelling + GitHub ations added). +- GitHub repository cleaned up (spelling + GitHub actions added). ## chart 1.3.0 diff --git a/R/chart-package.R b/R/chart-package.R index c3d5f8f..2a975e0 100644 --- a/R/chart-package.R +++ b/R/chart-package.R @@ -22,7 +22,7 @@ #' @importFrom cowplot theme_cowplot #' @importFrom rlang abort warn f_env f_lhs f_rhs is_true is_quosure #' @importFrom stats as.formula asOneSidedFormula -#' @importFrom utils modifyList +#' @importFrom utils .DollarNames apropos modifyList #' @importFrom pryr modify_lang #' @importFrom data.io label #' @importFrom ggplotify as.ggplot diff --git a/R/gg.R b/R/gg.R new file mode 100644 index 0000000..04cc375 --- /dev/null +++ b/R/gg.R @@ -0,0 +1,133 @@ +#' Make a ggplot function pipeable +#' +#' @description The function [gg()] should be used like this: `gg$geom_point()`. +#' It transforms on the fly an original {ggplot2} function supposed to be used +#' with the `+` operator (like `p + geom_point()`) into a pipeable version +#' (like `p %>% gg$geom_point()`). +#' +#' @param ggplot An object of class "ggplot" (or "theme"). +#' @param ... Further arguments passed to the the ggplot function (see Details). +#' @param x The `gg()`function. +#' @param name The name of the ggplot function to make pipeable. +#' @param pattern A regular expression to list matching names. +#' +#' @return The `gg()` function just returns an error message. When subsetted +#' with the name of a {ggplot2} function (e.g., `gg$geom_point()`), it +#' returns a modified version of that function in such a way that it can be +#' used with a pipe operator. +#' +#' @details The function returned by `gg$fun` is a modified version of the +#' function `fun` where a first argument `ggplot =` is added, and the +#' instruction `ggplot + ...` is added in its body. A message +#' is also added in the body to explicitly warn about these changes. All the +#' other arguments of `fun` remain valid and should keep their original meaning. +#' +#' The changes are done on the fly, and the original function `fun` is **not** +#' altered anywhere else (and in particular, no alteration is done in a package +#' or a namespace). When using this construct, make sure that: (1) you +#' understand what is done, (2) you are aware that you use an altered version of +#' the original function, (3) a bug or strange behavior may occur due to the +#' patch and the original author of the function is not responsible in this case +#' (the problem must be reported to the author of [gg()] and the maintainer of +#' the present package instead), and (4) the patched function exhibits an +#' additional argument and behaves differently to what is described in the help +#' page of the original, non-patched, function! +#' +#' @export +#' +#' @examples +#' library(ggplot2) +#' data(iris) +#' ggplot(aes(x = Petal.Length, y = Sepal.Length, col = Species) , data = iris) |> +#' gg$geom_point() |> +#' gg$labs(x = "Sepal length (mm)", y = "Petal length (mm)") +#' # Also try completion with gg$ +gg <- structure(function(ggplot, ...) { + stop("You must indicate gg$(), like gg$geom_point() or gg$labs()") +}, class = c("subsettable_gg", "function")) + +.make_pipeable_gg <- function(fun) { + if (is.function(fun)) { + f <- fun + } else { + fun_name <- as.character(fun) + f <- get0(fun_name, envir = parent.frame(2), mode = "function", + inherits = TRUE) + if (is.null(f)) + stop("Cannot found function '", fun_name, "'") + } + # Change arguments to prepend ggplot = + formals(f) <- c(pairlist(ggplot = NULL), formals(f)) + # Change body: prepend ggplot + {...} + patched_body <- body(function(ggplot, ...) { + "Warning: this is a patched version of a ggplot function to make it pipeable. Please, read ?gg first!" + ggplot + 1 + }) + patched_body[[3]][[3]] <- body(f) + body(f) <- patched_body + f +} + +#' @export +#' @rdname gg +#' @method $ subsettable_gg +`$.subsettable_gg` <- function(x, name) { + if (name == "title") + name <- "ggtitle" + .make_pipeable_gg(name) +} + +#' @export +#' @rdname gg +#' @method .DollarNames subsettable_gg +.DollarNames.subsettable_gg <- function(x, pattern = "") { + l <- list("annotate", "expand_limits", "guides", "labs", "lims", "theme", + "title", # Instead of ggtitle (special case!) + "xlab", "xlim", "ylab", "ylim") + l <- grep(pattern = pattern, l, value = TRUE) + l <- c(l, apropos(paste0("^geom_", pattern))) + l <- c(l, apropos(paste0("^annotation_", pattern))) + l <- c(l, apropos(paste0("^coord_", pattern))) + l <- c(l, apropos(paste0("^facet_", pattern))) + l <- c(l, apropos(paste0("^scale_", pattern))) + l <- c(l, apropos(paste0("^stat_", pattern))) + l <- c(l, apropos(paste0("^theme_", pattern))) + sort(l) +} + +# TODO: allow custom extensions to this list +# (trials that do not work for now) +# gg_help_handler <- function(type = c("completion", "parameter", "url"), +# topic, source, ...) { +# type <- match.arg(type) +# if (type == "completion") { +# # Just a trial +# list(title = "title", signature = "function(x, y)", returns = "1", +# description = "desc", details = "details", sections = "sections") +# } else if (type == "parameter") { +# list(args = c("x", "y"), arg_descriptions = c("x param", "y param")) +# } else if (type == "url") { +# "https://wp.sciviews.org" +# } +# } +# +# .DollarNames.subsettable_gg <- function(x, pattern = "") { +# l <- list("annotate", "expand_limits", "guides", "labs", "lims", "theme", +# "title", # Instead of ggtitle (special case!) +# "xlab", "xlim", "ylab", "ylim") +# l <- grep(pattern = pattern, l, value = TRUE) +# l <- c(l, apropos(paste0("^geom_", pattern))) +# l <- c(l, apropos(paste0("^annotation_", pattern))) +# l <- c(l, apropos(paste0("^coord_", pattern))) +# l <- c(l, apropos(paste0("^facet_", pattern))) +# l <- c(l, apropos(paste0("^scale_", pattern))) +# l <- c(l, apropos(paste0("^stat_", pattern))) +# l <- c(l, apropos(paste0("^theme_", pattern))) +# # This works and my object is recognized as a function, but with only ... arg +# #l <- paste0(sort(l), "(") +# # This does not work! +# #attr(l, "types") <- rep(6, length.out = length(l)) +# # This does not work! +# #attr(l, "helpHandler") <- gg_help_handler +# l +# } diff --git a/README.md b/README.md index e85b094..74cf528 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ -{chart} propose a formula interface to {ggplot2}, and it also homogenize plot outputs from base R plots, {lattice} and {ggplot}. If labels and/or units attributes are defined for variables in the data, they are used automatically to construct the label (with units) of the axes. +{chart} proposes a formula interface to {ggplot2}, and it also homogenize plot outputs from base R plots, {lattice} and {ggplot}. If labels and/or units attributes are defined for variables in the data, they are used automatically to construct the label (with units) of the axes. ## Installation diff --git a/TODO.md b/TODO.md index bf9dcc9..82f21c9 100644 --- a/TODO.md +++ b/TODO.md @@ -2,6 +2,8 @@ - `autoplot()` and `plot()` methods (+ `hist()`, etc.) +- Implement `.DollarNames()` to get a list of `chart()` types available, with a mechanism to add more types from additional packages. + - Refine themes and look at themes for {lattice} and base. - Use labels for base and {lattice} plots. diff --git a/inst/WORDLIST b/inst/WORDLIST index 7297e7e..dd4e923 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -4,5 +4,7 @@ devtools ggplot GitHub OSX +pipeable +subsettable thetics viridis diff --git a/man/gg.Rd b/man/gg.Rd new file mode 100644 index 0000000..ba4a32f --- /dev/null +++ b/man/gg.Rd @@ -0,0 +1,63 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/gg.R +\name{gg} +\alias{gg} +\alias{$.subsettable_gg} +\alias{.DollarNames.subsettable_gg} +\title{Make a ggplot function pipeable} +\usage{ +gg(ggplot, ...) + +\method{$}{subsettable_gg}(x, name) + +\method{.DollarNames}{subsettable_gg}(x, pattern = "") +} +\arguments{ +\item{ggplot}{An object of class "ggplot" (or "theme").} + +\item{...}{Further arguments passed to the the ggplot function (see Details).} + +\item{x}{The \code{gg()}function.} + +\item{name}{The name of the ggplot function to make pipeable.} + +\item{pattern}{A regular expression to list matching names.} +} +\value{ +The \code{gg()} function just returns an error message. When subsetted +with the name of a {ggplot2} function (e.g., \code{gg$geom_point()}), it +returns a modified version of that function in such a way that it can be +used with a pipe operator. +} +\description{ +The function \code{\link[=gg]{gg()}} should be used like this: \code{gg$geom_point()}. +It transforms on the fly an original {ggplot2} function supposed to be used +with the \code{+} operator (like \code{p + geom_point()}) into a pipeable version +(like \code{p \%>\% gg$geom_point()}). +} +\details{ +The function returned by \code{gg$fun} is a modified version of the +function \code{fun} where a first argument \verb{ggplot =} is added, and the +instruction \code{ggplot + ...} is added in its body. A message +is also added in the body to explicitly warn about these changes. All the +other arguments of \code{fun} remain valid and should keep their original meaning. + +The changes are done on the fly, and the original function \code{fun} is \strong{not} +altered anywhere else (and in particular, no alteration is done in a package +or a namespace). When using this construct, make sure that: (1) you +understand what is done, (2) you are aware that you use an altered version of +the original function, (3) a bug or strange behavior may occur due to the +patch and the original author of the function is not responsible in this case +(the problem must be reported to the author of \code{\link[=gg]{gg()}} and the maintainer of +the present package instead), and (4) the patched function exhibits an +additional argument and behaves differently to what is described in the help +page of the original, non-patched, function! +} +\examples{ +library(ggplot2) +data(iris) +ggplot(aes(x = Petal.Length, y = Sepal.Length, col = Species) , data = iris) |> + gg$geom_point() |> + gg$labs(x = "Sepal length (mm)", y = "Petal length (mm)") +# Also try completion with gg$ +}