├── .github ├── .gitignore └── workflows │ ├── test-coverage.yaml │ ├── pkgdown.yaml │ └── R-CMD-check.yaml ├── .gitignore ├── LICENSE ├── man ├── figures │ ├── README-remap-1.png │ ├── README-data_1-1.png │ ├── README-data_2-1.png │ ├── README-data_3-1.png │ ├── README-dataset-1.png │ ├── README-boxplot_ggplot-1.png │ ├── README-boxplot_label_1-1.png │ ├── README-boxplot_label_2-1.png │ └── README-boxplot_ggbuilder-1.png ├── layer_spec.Rd ├── layer.Rd ├── ggbuilder-package.Rd ├── remap.Rd ├── plot_data.Rd ├── stat_.Rd └── geom_.Rd ├── .Rbuildignore ├── R ├── util.R ├── layer-spec.R ├── ggbuilder-package.R ├── remap.R ├── plot-data.R ├── stat.R ├── geom.R ├── layer.R └── plot-data-verbs.R ├── codecov.yml ├── tests ├── testthat.R └── testthat │ ├── test-layer.R │ └── _snaps │ └── layer │ ├── single-geom.svg │ ├── single-stat-and-geom.svg │ ├── plot-data-with-several-verbs-and-remap.svg │ ├── plot-data-with-a-verb.svg │ └── plot-data-with-a-function.svg ├── ggbuilder.Rproj ├── _pkgdown.yml ├── DESCRIPTION ├── LICENSE.md ├── NAMESPACE ├── README.Rmd └── README.md /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | *~ 4 | docs 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2022 2 | COPYRIGHT HOLDER: ggbuilder authors 3 | -------------------------------------------------------------------------------- /man/figures/README-remap-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggbuilder/main/man/figures/README-remap-1.png -------------------------------------------------------------------------------- /man/figures/README-data_1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggbuilder/main/man/figures/README-data_1-1.png -------------------------------------------------------------------------------- /man/figures/README-data_2-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggbuilder/main/man/figures/README-data_2-1.png -------------------------------------------------------------------------------- /man/figures/README-data_3-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggbuilder/main/man/figures/README-data_3-1.png -------------------------------------------------------------------------------- /man/figures/README-dataset-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggbuilder/main/man/figures/README-dataset-1.png -------------------------------------------------------------------------------- /man/figures/README-boxplot_ggplot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggbuilder/main/man/figures/README-boxplot_ggplot-1.png -------------------------------------------------------------------------------- /man/figures/README-boxplot_label_1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggbuilder/main/man/figures/README-boxplot_label_1-1.png -------------------------------------------------------------------------------- /man/figures/README-boxplot_label_2-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggbuilder/main/man/figures/README-boxplot_label_2-1.png -------------------------------------------------------------------------------- /man/figures/README-boxplot_ggbuilder-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggbuilder/main/man/figures/README-boxplot_ggbuilder-1.png -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^ggbuilder\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^LICENSE\.md$ 4 | ^README\.Rmd$ 5 | ^\.github$ 6 | ^_pkgdown\.yml$ 7 | ^docs$ 8 | ^pkgdown$ 9 | ^codecov\.yml$ 10 | -------------------------------------------------------------------------------- /R/util.R: -------------------------------------------------------------------------------- 1 | # set missing values from x to provided default values 2 | defaults = function(x, defaults) { 3 | c(x, defaults[setdiff(names(defaults), names(x))]) 4 | } 5 | 6 | stop0 = function(...) { 7 | stop(..., call. = FALSE) 8 | } 9 | 10 | cat0 = function(...) { 11 | cat(..., sep = "") 12 | } 13 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | target: auto 8 | threshold: 1% 9 | informational: true 10 | patch: 11 | default: 12 | target: auto 13 | threshold: 1% 14 | informational: true 15 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | # This file is part of the standard setup for testthat. 2 | # It is recommended that you do not modify it. 3 | # 4 | # Where should you do additional test configuration? 5 | # Learn more about the roles of various files in: 6 | # * https://r-pkgs.org/tests.html 7 | # * https://testthat.r-lib.org/reference/test_package.html#special-files 8 | 9 | library(testthat) 10 | library(ggbuilder) 11 | 12 | test_check("ggbuilder") 13 | -------------------------------------------------------------------------------- /ggbuilder.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | LineEndingConversion: Posix 18 | 19 | BuildType: Package 20 | PackageUseDevtools: Yes 21 | PackageInstallArgs: --no-multiarch --with-keep.source 22 | PackageRoxygenize: rd,collate,namespace 23 | -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: https://mjskay.github.io/ggbuilder/ 2 | template: 3 | bootstrap: 5 4 | 5 | reference: 6 | - title: Package overview 7 | desc: Overview of ggbuilder 8 | contents: 9 | - ggbuilder-package 10 | 11 | - title: Pipable layer construction functions 12 | desc: Composable functions for constructing ggplot2 layers using a data-flow approach 13 | contents: 14 | - geom_ 15 | - stat_ 16 | - plot_data 17 | - remap 18 | 19 | - title: Low-level layer constructors 20 | desc: Low-level functions for constructing layers and layer specifications (typically not used directly) 21 | contents: 22 | - new_layer 23 | - new_layer_spec 24 | -------------------------------------------------------------------------------- /man/layer_spec.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/layer-spec.R 3 | \name{layer_spec} 4 | \alias{layer_spec} 5 | \alias{new_layer_spec} 6 | \title{Pipable ggplot2 layer specifications} 7 | \usage{ 8 | new_layer_spec() 9 | } 10 | \value{ 11 | A ggbuilder \link{layer_spec} representing a layer to be constructed by \code{\link[=new_layer]{new_layer()}}. 12 | } 13 | \description{ 14 | A specification for a \link[ggplot2:layer]{ggplot2::layer} built from a \link{layer_spec} and intended for 15 | piping. Typically constructed using \code{\link[=geom_]{geom_()}} or \code{\link[=stat_]{stat_()}}, not this function. 16 | } 17 | \examples{ 18 | 19 | # See geom_() or stat_() for examples; this function 20 | # should not usually be used directly. 21 | 22 | } 23 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: ggbuilder 2 | Title: A Data Flow Pipeline Approach to Building ggplot2 Layers 3 | Version: 0.0.0.9000 4 | Authors@R: 5 | person("Matthew", "Kay", , "mjskay@northwestern.edu", role = c("aut", "cre")) 6 | Description: An (experimental) alternative way to build ggplot2 layers, where elements 7 | of the layer, such as data, mappings, statistical transformations, and geometries 8 | are specified in the order their data is processed. 9 | Depends: 10 | R (>= 4.1) 11 | Imports: 12 | ggplot2, 13 | rlang 14 | Suggests: 15 | dplyr, 16 | tidyr, 17 | generics, 18 | vdiffr, 19 | scales, 20 | testthat (>= 3.0.0), 21 | covr 22 | License: MIT + file LICENSE 23 | Encoding: UTF-8 24 | Roxygen: list(markdown = TRUE) 25 | RoxygenNote: 7.2.1 26 | Config/testthat/edition: 3 27 | URL: https://mjskay.github.io/ggbuilder/ 28 | -------------------------------------------------------------------------------- /.github/workflows/test-coverage.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | 9 | name: test-coverage 10 | 11 | jobs: 12 | test-coverage: 13 | runs-on: ubuntu-latest 14 | env: 15 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - uses: r-lib/actions/setup-r@v2 21 | with: 22 | use-public-rspm: true 23 | 24 | - uses: r-lib/actions/setup-r-dependencies@v2 25 | with: 26 | extra-packages: any::covr 27 | needs: coverage 28 | 29 | - name: Test coverage 30 | run: covr::codecov(quiet = FALSE) 31 | shell: Rscript {0} 32 | -------------------------------------------------------------------------------- /R/layer-spec.R: -------------------------------------------------------------------------------- 1 | #' Pipable ggplot2 layer specifications 2 | #' 3 | #' A specification for a [ggplot2::layer] built from a [ggbuilder::layer_spec] and intended for 4 | #' piping. Typically constructed using [geom_()] or [stat_()], not this function. 5 | #' 6 | #' @returns 7 | #' 8 | #' A ggbuilder [layer_spec] representing a layer to be constructed by [new_layer()]. 9 | #' 10 | #' @examples 11 | #' 12 | #' # See geom_() or stat_() for examples; this function 13 | #' # should not usually be used directly. 14 | #' 15 | #' @import ggplot2 16 | #' @importFrom rlang expr 17 | #' @name layer_spec 18 | #' @export 19 | new_layer_spec = function() { 20 | structure(list( 21 | params = list(), 22 | mapping_stat = aes(), 23 | mapping_geom = aes(), 24 | mapping_final = aes() 25 | ), class = "ggbuilder_layer_spec") 26 | } 27 | 28 | 29 | # printing ---------------------------------------------------------------- 30 | 31 | #' @export 32 | print.ggbuilder_layer_spec = function(x, ...) { 33 | cat0(":\n") 34 | print(unclass(x), ...) 35 | invisible(x) 36 | } 37 | -------------------------------------------------------------------------------- /R/ggbuilder-package.R: -------------------------------------------------------------------------------- 1 | #' A Data Flow Pipeline Approach to Building ggplot2 Layers 2 | #' 3 | #' @docType package 4 | #' @name ggbuilder-package 5 | #' @aliases ggbuilder 6 | #' 7 | #' @description 8 | #' 9 | #' \pkg{ggbuilder} provides a pipeline-based approach for building individual 10 | #' \pkg{ggplot2} layers, analogous to \pkg{dplyr}-like data transformation pipelines. 11 | #' 12 | #' @details 13 | #' 14 | #' As an alternative to \pkg{ggplot2} functions for controlling aesthetic evaluation 15 | #' like [stage()], \pkg{ggbuilder} allows you to use function piping (i.e. [`|>`]) 16 | #' to chain data transformations, [ggplot2::Stat]s, and [ggplot2::Geom]s together 17 | #' in order to construct layers. 18 | #' 19 | #' - See [geom_()] and [stat_()] for how to construct and chain geoms and stats 20 | #' using \pkg{ggbuilder} 21 | #' 22 | #' - See [plot_data()] for how to add data transformations into a layer definition 23 | #' 24 | #' - See [remap()] for how to apply transformations to aesthetics after scales 25 | #' have been calculated (analogous to [ggplot2::after_scale()]). 26 | #' 27 | NULL 28 | -------------------------------------------------------------------------------- /man/layer.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/layer.R 3 | \name{layer} 4 | \alias{layer} 5 | \alias{new_layer} 6 | \alias{new_layer.ggbuilder_layer} 7 | \alias{new_layer.ggbuilder_layer_spec} 8 | \title{Pipable ggplot2 layers} 9 | \usage{ 10 | new_layer(x) 11 | 12 | \method{new_layer}{ggbuilder_layer}(x) 13 | 14 | \method{new_layer}{ggbuilder_layer_spec}(x) 15 | } 16 | \arguments{ 17 | \item{x}{One of: 18 | \itemize{ 19 | \item A \pkg{ggbuilder} \link{layer_spec} 20 | \item A \pkg{ggbuilder} \link{layer} 21 | }} 22 | } 23 | \value{ 24 | A ggbuilder \link{layer}, which is also a \link[ggplot2:layer]{ggplot2::layer}, and which can be 25 | added to a \code{\link[=ggplot]{ggplot()}} object or piped into other ggbuilder functions. 26 | } 27 | \description{ 28 | A \link[ggplot2:layer]{ggplot2::layer} built from a \link{layer_spec} and intended for 29 | piping. Typically constructed using \code{\link[=geom_]{geom_()}} or \code{\link[=stat_]{stat_()}}, not this function. 30 | } 31 | \examples{ 32 | 33 | # See geom_() or stat_() for examples; this function 34 | # should not usually be used directly. 35 | 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2022 ggbuilder authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /man/ggbuilder-package.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/ggbuilder-package.R 3 | \docType{package} 4 | \name{ggbuilder-package} 5 | \alias{ggbuilder-package} 6 | \alias{ggbuilder} 7 | \title{A Data Flow Pipeline Approach to Building ggplot2 Layers} 8 | \description{ 9 | \pkg{ggbuilder} provides a pipeline-based approach for building individual 10 | \pkg{ggplot2} layers, analogous to \pkg{dplyr}-like data transformation pipelines. 11 | } 12 | \details{ 13 | As an alternative to \pkg{ggplot2} functions for controlling aesthetic evaluation 14 | like \code{\link[=stage]{stage()}}, \pkg{ggbuilder} allows you to use function piping (i.e. \code{\link{|>}}) 15 | to chain data transformations, \link[ggplot2:ggplot2-ggproto]{ggplot2::Stat}s, and \link[ggplot2:ggplot2-ggproto]{ggplot2::Geom}s together 16 | in order to construct layers. 17 | \itemize{ 18 | \item See \code{\link[=geom_]{geom_()}} and \code{\link[=stat_]{stat_()}} for how to construct and chain geoms and stats 19 | using \pkg{ggbuilder} 20 | \item See \code{\link[=plot_data]{plot_data()}} for how to add data transformations into a layer definition 21 | \item See \code{\link[=remap]{remap()}} for how to apply transformations to aesthetics after scales 22 | have been calculated (analogous to \code{\link[ggplot2:aes_eval]{ggplot2::after_scale()}}). 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | release: 9 | types: [published] 10 | workflow_dispatch: 11 | 12 | name: pkgdown 13 | 14 | jobs: 15 | pkgdown: 16 | runs-on: ubuntu-latest 17 | # Only restrict concurrency for non-PR jobs 18 | concurrency: 19 | group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }} 20 | env: 21 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 22 | steps: 23 | - uses: actions/checkout@v2 24 | 25 | - uses: r-lib/actions/setup-pandoc@v2 26 | 27 | - uses: r-lib/actions/setup-r@v2 28 | with: 29 | use-public-rspm: true 30 | 31 | - uses: r-lib/actions/setup-r-dependencies@v2 32 | with: 33 | extra-packages: any::pkgdown, local::. 34 | needs: website 35 | 36 | - name: Build site 37 | run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) 38 | shell: Rscript {0} 39 | 40 | - name: Deploy to GitHub pages 🚀 41 | if: github.event_name != 'pull_request' 42 | uses: JamesIves/github-pages-deploy-action@4.1.4 43 | with: 44 | clean: false 45 | branch: gh-pages 46 | folder: docs 47 | -------------------------------------------------------------------------------- /man/remap.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/remap.R 3 | \name{remap} 4 | \alias{remap} 5 | \alias{remap.ggbuilder_layer} 6 | \alias{remap.ggbuilder_layer_spec} 7 | \title{Remap aesthetics after scales are applied} 8 | \usage{ 9 | remap(x, mapping) 10 | 11 | \method{remap}{ggbuilder_layer}(x, mapping) 12 | 13 | \method{remap}{ggbuilder_layer_spec}(x, mapping) 14 | } 15 | \arguments{ 16 | \item{x}{One of: 17 | \itemize{ 18 | \item A \pkg{ggbuilder} \link{layer} 19 | \item A \pkg{ggbuilder} \link{layer_spec} 20 | }} 21 | 22 | \item{mapping}{Set of aesthetic mappings created by \code{\link[=aes]{aes()}} or \code{\link[=aes_]{aes_()}}.} 23 | } 24 | \value{ 25 | A ggbuilder \link{layer} with aesthetics provided in \code{mapping} applied after 26 | scales are computed, using \code{\link[ggplot2:aes_eval]{ggplot2::after_scale()}} 27 | } 28 | \description{ 29 | Add this to the end of a \pkg{ggbuilder} pipeline to remap layer aesthetics 30 | after the scale functions have been applied. Analogous to \code{\link[ggplot2:aes_eval]{ggplot2::after_scale()}}. 31 | } 32 | \examples{ 33 | 34 | library(ggplot2) 35 | 36 | set.seed(123456) 37 | df = data.frame(condition = c("A", "B", "C"), response = round(rnorm(30, 1:3), 1)) 38 | 39 | df |> 40 | ggplot(aes(x = condition, y = response, color = condition)) + 41 | geom_boxplot() + 42 | stat_("boxplot", aes(y = response)) |> 43 | geom_("label", aes(y = ymax, label = ymax)) |> 44 | remap(aes(fill = scales::alpha(color, 0.1))) + 45 | theme_light() 46 | 47 | } 48 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | 9 | name: R-CMD-check 10 | 11 | jobs: 12 | R-CMD-check: 13 | runs-on: ${{ matrix.config.os }} 14 | 15 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | config: 21 | - {os: macOS-latest, r: 'release'} 22 | - {os: windows-latest, r: 'release'} 23 | - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} 24 | - {os: ubuntu-latest, r: 'release'} 25 | - {os: ubuntu-latest, r: 'oldrel-1'} 26 | 27 | env: 28 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 29 | R_KEEP_PKG_SOURCE: yes 30 | 31 | steps: 32 | - uses: actions/checkout@v2 33 | 34 | - uses: r-lib/actions/setup-pandoc@v2 35 | 36 | - uses: r-lib/actions/setup-r@v2 37 | with: 38 | r-version: ${{ matrix.config.r }} 39 | http-user-agent: ${{ matrix.config.http-user-agent }} 40 | use-public-rspm: true 41 | 42 | - uses: r-lib/actions/setup-r-dependencies@v2 43 | with: 44 | extra-packages: any::rcmdcheck 45 | needs: check 46 | 47 | - uses: r-lib/actions/check-r-package@v2 48 | with: 49 | upload-snapshots: true 50 | -------------------------------------------------------------------------------- /R/remap.R: -------------------------------------------------------------------------------- 1 | #' Remap aesthetics after scales are applied 2 | #' 3 | #' Add this to the end of a \pkg{ggbuilder} pipeline to remap layer aesthetics 4 | #' after the scale functions have been applied. Analogous to [ggplot2::after_scale()]. 5 | #' 6 | #' @param x One of: 7 | #' - A \pkg{ggbuilder} [layer] 8 | #' - A \pkg{ggbuilder} [layer_spec] 9 | #' @param mapping Set of aesthetic mappings created by [aes()] or [aes_()]. 10 | #' 11 | #' @returns 12 | #' A ggbuilder [layer] with aesthetics provided in `mapping` applied after 13 | #' scales are computed, using [ggplot2::after_scale()] 14 | #' 15 | #' @examples 16 | #' 17 | #' library(ggplot2) 18 | #' 19 | #' set.seed(123456) 20 | #' df = data.frame(condition = c("A", "B", "C"), response = round(rnorm(30, 1:3), 1)) 21 | #' 22 | #' df |> 23 | #' ggplot(aes(x = condition, y = response, color = condition)) + 24 | #' geom_boxplot() + 25 | #' stat_("boxplot", aes(y = response)) |> 26 | #' geom_("label", aes(y = ymax, label = ymax)) |> 27 | #' remap(aes(fill = scales::alpha(color, 0.1))) + 28 | #' theme_light() 29 | #' 30 | #' @name remap 31 | NULL 32 | 33 | new_remap = function(mapping, .layer_spec = new_layer_spec()) { 34 | .layer_spec$mapping_final = defaults(mapping, .layer_spec$mapping_final) 35 | new_layer(.layer_spec) 36 | } 37 | 38 | #' @rdname remap 39 | #' @export 40 | remap = function(x, mapping) { 41 | UseMethod("remap") 42 | } 43 | 44 | #' @rdname remap 45 | #' @export 46 | remap.ggbuilder_layer = function(x, mapping) { 47 | new_remap(mapping, .layer_spec = attr(x, "ggbuilder_layer_spec")) 48 | } 49 | 50 | #' @rdname remap 51 | #' @export 52 | remap.ggbuilder_layer_spec = function(x, mapping) { 53 | new_remap(mapping, .layer_spec = x) 54 | } 55 | -------------------------------------------------------------------------------- /R/plot-data.R: -------------------------------------------------------------------------------- 1 | #' Pipable ggplot2 layer data 2 | #' 3 | #' A wrapper around data to be passed to the `data` argument of [ggplot2::layer()]. 4 | #' Pipe it into a \pkg{ggbuilder} [layer] to set the data used by that layer. 5 | #' 6 | #' @param data One of: 7 | #' - `NULL`: Use the data from the [ggplot2()] object. 8 | #' - A function: apply this function to the data from the [ggplot2()] object 9 | #' when constructing the data for the layer. 10 | #' - A [data.frame()]: use this data frame as the data for the layer. 11 | #' 12 | #' @returns 13 | #' 14 | #' A ggbuilder [plot_data], which can be piped into other ggbuilder 15 | #' functions. Can also be piped into \pkg{dplyr} and \pkg{tidyr} verbs in order 16 | #' to construct a new [plot_data] that will apply those verbs to the data. 17 | #' 18 | #' @examples 19 | #' 20 | #' library(ggplot2) 21 | #' 22 | #' set.seed(123456) 23 | #' df = data.frame(condition = c("A", "B", "C"), response = round(rnorm(30, 1:3), 1)) 24 | #' 25 | #' df |> 26 | #' ggplot(aes(x = condition, y = response, color = condition)) + 27 | #' geom_boxplot() + 28 | #' plot_data(\(x) x[x$condition %in% c("B", "C"),]) |> 29 | #' stat_("boxplot", aes(y = response)) |> 30 | #' geom_("label", aes(y = ymax, label = ymax)) 31 | #' 32 | #' df |> 33 | #' ggplot(aes(x = condition, y = response, color = condition)) + 34 | #' geom_boxplot() + 35 | #' plot_data() |> 36 | #' dplyr::filter(condition %in% c("B", "C")) |> 37 | #' dplyr::group_by(condition) |> 38 | #' dplyr::slice_max(response) |> 39 | #' geom_("label", aes(label = response)) 40 | #' 41 | #' @import ggplot2 42 | #' @importFrom rlang expr 43 | #' @export 44 | plot_data = function(data = NULL) { 45 | structure(list(data = data), class = "ggbuilder_plot_data") 46 | } 47 | -------------------------------------------------------------------------------- /man/plot_data.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/plot-data.R 3 | \name{plot_data} 4 | \alias{plot_data} 5 | \title{Pipable ggplot2 layer data} 6 | \usage{ 7 | plot_data(data = NULL) 8 | } 9 | \arguments{ 10 | \item{data}{One of: 11 | \itemize{ 12 | \item \code{NULL}: Use the data from the \code{\link[=ggplot2]{ggplot2()}} object. 13 | \item A function: apply this function to the data from the \code{\link[=ggplot2]{ggplot2()}} object 14 | when constructing the data for the layer. 15 | \item A \code{\link[=data.frame]{data.frame()}}: use this data frame as the data for the layer. 16 | }} 17 | } 18 | \value{ 19 | A ggbuilder \link{plot_data}, which can be piped into other ggbuilder 20 | functions. Can also be piped into \pkg{dplyr} and \pkg{tidyr} verbs in order 21 | to construct a new \link{plot_data} that will apply those verbs to the data. 22 | } 23 | \description{ 24 | A wrapper around data to be passed to the \code{data} argument of \code{\link[ggplot2:layer]{ggplot2::layer()}}. 25 | Pipe it into a \pkg{ggbuilder} \link{layer} to set the data used by that layer. 26 | } 27 | \examples{ 28 | 29 | library(ggplot2) 30 | 31 | set.seed(123456) 32 | df = data.frame(condition = c("A", "B", "C"), response = round(rnorm(30, 1:3), 1)) 33 | 34 | df |> 35 | ggplot(aes(x = condition, y = response, color = condition)) + 36 | geom_boxplot() + 37 | plot_data(\(x) x[x$condition \%in\% c("B", "C"),]) |> 38 | stat_("boxplot", aes(y = response)) |> 39 | geom_("label", aes(y = ymax, label = ymax)) 40 | 41 | df |> 42 | ggplot(aes(x = condition, y = response, color = condition)) + 43 | geom_boxplot() + 44 | plot_data() |> 45 | dplyr::filter(condition \%in\% c("B", "C")) |> 46 | dplyr::group_by(condition) |> 47 | dplyr::slice_max(response) |> 48 | geom_("label", aes(label = response)) 49 | 50 | } 51 | -------------------------------------------------------------------------------- /tests/testthat/test-layer.R: -------------------------------------------------------------------------------- 1 | test_that("layer construction works", { 2 | skip_if_not_installed("vdiffr") 3 | 4 | df = data.frame(response_time = 1:30, condition = c("A", "B", "C")) 5 | p = ggplot(df) 6 | 7 | vdiffr::expect_doppelganger("single geom", 8 | p + geom_("point", aes(x = condition, y = response_time), size = 3) 9 | ) 10 | 11 | 12 | vdiffr::expect_doppelganger("single stat and geom", 13 | p + stat_("identity", aes(x = condition, y = response_time)) |> 14 | geom_("point", size = 3) 15 | ) 16 | 17 | vdiffr::expect_doppelganger("plot_data() with a function", 18 | p + 19 | geom_boxplot(aes(x = condition, y = response_time, color = condition)) + 20 | plot_data(\(x) x[x$condition %in% c("B", "C"),]) |> 21 | stat_("boxplot", aes(x = condition, y = response_time, color = condition)) |> 22 | geom_("label", aes(y = ymax, label = ymax), size = 5) 23 | ) 24 | 25 | 26 | skip_if_not_installed("dplyr") 27 | 28 | vdiffr::expect_doppelganger("plot_data() with a verb", 29 | p + 30 | geom_boxplot(aes(x = condition, y = response_time, color = condition)) + 31 | plot_data() |> 32 | dplyr::filter(condition %in% c("B", "C")) |> 33 | stat_("boxplot", aes(x = condition, y = response_time, color = condition)) |> 34 | geom_("label", aes(y = ymax, label = ymax), size = 5) 35 | ) 36 | 37 | vdiffr::expect_doppelganger("plot_data() with several verbs and remap()", 38 | df |> 39 | ggplot(aes(x = condition, y = response_time, color = condition)) + 40 | stat_("boxplot") + 41 | plot_data() |> 42 | dplyr::filter(condition %in% c("B", "C")) |> 43 | dplyr::group_by(condition) |> 44 | dplyr::slice_max(response_time) |> 45 | geom_("label", aes(label = response_time), size = 5, show.legend = FALSE) |> 46 | remap(aes(fill = scales::alpha(color, .1))) 47 | ) 48 | 49 | }) 50 | -------------------------------------------------------------------------------- /man/stat_.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/stat.R 3 | \name{stat_} 4 | \alias{stat_} 5 | \title{Pipable ggplot2 stats} 6 | \arguments{ 7 | \item{x}{One of: 8 | \itemize{ 9 | \item A string: The suffix of a \code{stat_XXX} function used to create this stat. 10 | \item A \pkg{ggbuilder} \link{layer}, \link{layer_spec}, or \link{plot_data} object representing 11 | earlier data and data transformations used as input to this stat. In this 12 | case, the second argument should be the suffix of a \code{stat_XXX} function 13 | used to create this stat. 14 | }} 15 | 16 | \item{...}{arguments passed on to the stat's constructor.} 17 | } 18 | \value{ 19 | A ggbuilder \link{layer}, which is also a \link[ggplot2:layer]{ggplot2::layer}, and which can be 20 | added to a \code{\link[=ggplot]{ggplot()}} object or piped into other ggbuilder functions. 21 | } 22 | \description{ 23 | A \link[ggplot2:aes_eval]{ggplot2::stat} intended for piping. 24 | } 25 | \details{ 26 | \code{stat_()} can be used to replace typical \code{stat_XXX()} calls in \pkg{ggplot2} 27 | code to make them pipable by using the \code{XXX} suffix as a string as the first 28 | argument to \code{stat_()}. 29 | 30 | For example, \code{stat_boxplot(...)} would become \code{stat_("boxplot", ...)}. 31 | The primary difference between the former and the latter is that the latter 32 | is both a \link[ggplot2:layer]{ggplot2::layer} \emph{and} a ggbuilder \link{layer} containing a \link{layer_spec}. 33 | The contained \link{layer_spec} stores information on how to construct the 34 | stat, which allows it to be combined with other objects (e.g. by piping it 35 | into \code{geom_()}) to construct more complex layers. See \emph{Examples}. 36 | } 37 | \examples{ 38 | library(ggplot2) 39 | 40 | set.seed(123456) 41 | df = data.frame(condition = c("A", "B", "C"), response = round(rnorm(30, 1:3), 1)) 42 | 43 | df |> 44 | ggplot(aes(x = condition, y = response, color = condition)) + 45 | geom_boxplot() + 46 | stat_("boxplot", aes(y = response)) |> 47 | geom_("label", aes(y = ymax, label = ymax)) |> 48 | remap(aes(fill = scales::alpha(color, 0.1))) + 49 | theme_light() 50 | 51 | } 52 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method(dplyr::arrange,ggbuilder_plot_data) 4 | S3method(dplyr::count,ggbuilder_plot_data) 5 | S3method(dplyr::distinct,ggbuilder_plot_data) 6 | S3method(dplyr::do,ggbuilder_plot_data) 7 | S3method(dplyr::filter,ggbuilder_plot_data) 8 | S3method(dplyr::full_join,ggbuilder_plot_data) 9 | S3method(dplyr::group_by,ggbuilder_plot_data) 10 | S3method(dplyr::inner_join,ggbuilder_plot_data) 11 | S3method(dplyr::left_join,ggbuilder_plot_data) 12 | S3method(dplyr::mutate,ggbuilder_plot_data) 13 | S3method(dplyr::pull,ggbuilder_plot_data) 14 | S3method(dplyr::right_join,ggbuilder_plot_data) 15 | S3method(dplyr::select,ggbuilder_plot_data) 16 | S3method(dplyr::slice,ggbuilder_plot_data) 17 | S3method(dplyr::slice_head,ggbuilder_plot_data) 18 | S3method(dplyr::slice_max,ggbuilder_plot_data) 19 | S3method(dplyr::slice_min,ggbuilder_plot_data) 20 | S3method(dplyr::slice_sample,ggbuilder_plot_data) 21 | S3method(dplyr::slice_tail,ggbuilder_plot_data) 22 | S3method(dplyr::summarise,ggbuilder_plot_data) 23 | S3method(dplyr::summarize,ggbuilder_plot_data) 24 | S3method(dplyr::transmute,ggbuilder_plot_data) 25 | S3method(generics::intersect,ggbuilder_plot_data) 26 | S3method(generics::setdiff,ggbuilder_plot_data) 27 | S3method(generics::union,ggbuilder_plot_data) 28 | S3method(geom_,character) 29 | S3method(geom_,ggbuilder_layer) 30 | S3method(geom_,ggbuilder_layer_spec) 31 | S3method(geom_,ggbuilder_plot_data) 32 | S3method(new_layer,ggbuilder_layer) 33 | S3method(new_layer,ggbuilder_layer_spec) 34 | S3method(plot_data_sequence,"NULL") 35 | S3method(plot_data_sequence,"function") 36 | S3method(plot_data_sequence,data.frame) 37 | S3method(print,ggbuilder_layer) 38 | S3method(print,ggbuilder_layer_spec) 39 | S3method(remap,ggbuilder_layer) 40 | S3method(remap,ggbuilder_layer_spec) 41 | S3method(stat_,character) 42 | S3method(stat_,ggbuilder_layer) 43 | S3method(stat_,ggbuilder_layer_spec) 44 | S3method(stat_,ggbuilder_plot_data) 45 | S3method(tidyr::expand,ggbuilder_plot_data) 46 | S3method(tidyr::fill,ggbuilder_plot_data) 47 | S3method(tidyr::pivot_longer,ggbuilder_plot_data) 48 | S3method(tidyr::pivot_wider,ggbuilder_plot_data) 49 | S3method(utils::head,ggbuilder_plot_data) 50 | S3method(utils::tail,ggbuilder_plot_data) 51 | export(geom_) 52 | export(new_layer) 53 | export(new_layer_spec) 54 | export(plot_data) 55 | export(remap) 56 | export(stat_) 57 | import(ggplot2) 58 | importFrom(rlang,expr) 59 | -------------------------------------------------------------------------------- /man/geom_.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/geom.R 3 | \name{geom_} 4 | \alias{geom_} 5 | \alias{geom_.character} 6 | \alias{geom_.ggbuilder_layer} 7 | \alias{geom_.ggbuilder_layer_spec} 8 | \alias{geom_.ggbuilder_plot_data} 9 | \title{Pipable ggplot2 geoms} 10 | \usage{ 11 | geom_(x, ...) 12 | 13 | \method{geom_}{character}(x, ...) 14 | 15 | \method{geom_}{ggbuilder_layer}(x, ...) 16 | 17 | \method{geom_}{ggbuilder_layer_spec}(x, ...) 18 | 19 | \method{geom_}{ggbuilder_plot_data}(x, ...) 20 | } 21 | \arguments{ 22 | \item{x}{One of: 23 | \itemize{ 24 | \item A string: The suffix of a \code{geom_XXX} function used to create this geom. 25 | \item A \pkg{ggbuilder} \link{layer}, \link{layer_spec}, or \link{plot_data} object representing 26 | earlier data and data transformations used as input to this geom. In this 27 | case, the second argument should be the suffix of a \code{geom_XXX} function 28 | used to create this geom. 29 | }} 30 | 31 | \item{...}{arguments passed on to the geom's constructor.} 32 | } 33 | \value{ 34 | A ggbuilder \link{layer}, which is also a \link[ggplot2:layer]{ggplot2::layer}, and which can be 35 | added to a \code{\link[=ggplot]{ggplot()}} object or piped into other ggbuilder functions. 36 | } 37 | \description{ 38 | A \code{\link[ggplot2:layer]{ggplot2::layer()}} containing a \link[ggplot2:ggplot2-ggproto]{ggplot2::Geom}; intended for piping. 39 | } 40 | \details{ 41 | \code{geom_()} can be used to replace typical \code{geom_XXX()} calls in \pkg{ggplot2} 42 | code to make them pipable by using the \code{XXX} suffix as a string as the first 43 | argument to \code{geom_()}. 44 | 45 | For example, \code{geom_boxplot(...)} would become \code{geom_("boxplot", ...)}. 46 | The primary difference between the former and the latter is that the latter 47 | is both a \link[ggplot2:layer]{ggplot2::layer} \emph{and} a ggbuilder \link{layer} containing a \link{layer_spec}. 48 | The contained \link{layer_spec} stores information on how to construct the 49 | geom, which allows it to be combined with other objects (e.g. by piping it 50 | into \code{remap()}) to construct more complex layers. See \emph{Examples}. 51 | } 52 | \examples{ 53 | library(ggplot2) 54 | 55 | set.seed(123456) 56 | df = data.frame(condition = c("A", "B", "C"), response = round(rnorm(30, 1:3), 1)) 57 | 58 | df |> 59 | ggplot(aes(x = condition, y = response, color = condition)) + 60 | geom_boxplot() + 61 | stat_("boxplot", aes(y = response)) |> 62 | geom_("label", aes(y = ymax, label = ymax)) |> 63 | remap(aes(fill = scales::alpha(color, 0.1))) + 64 | theme_light() 65 | 66 | } 67 | -------------------------------------------------------------------------------- /R/stat.R: -------------------------------------------------------------------------------- 1 | #' Pipable ggplot2 stats 2 | #' 3 | #' A [ggplot2::stat] intended for piping. 4 | #' 5 | #' @param x One of: 6 | #' - A string: The suffix of a `stat_XXX` function used to create this stat. 7 | #' - A \pkg{ggbuilder} [layer], [layer_spec], or [plot_data] object representing 8 | #' earlier data and data transformations used as input to this stat. In this 9 | #' case, the second argument should be the suffix of a `stat_XXX` function 10 | #' used to create this stat. 11 | #' @param ... arguments passed on to the stat's constructor. 12 | #' 13 | #' @details 14 | #' `stat_()` can be used to replace typical `stat_XXX()` calls in \pkg{ggplot2} 15 | #' code to make them pipable by using the `XXX` suffix as a string as the first 16 | #' argument to `stat_()`. 17 | #' 18 | #' For example, `stat_boxplot(...)` would become `stat_("boxplot", ...)`. 19 | #' The primary difference between the former and the latter is that the latter 20 | #' is both a [ggplot2::layer] *and* a ggbuilder [layer] containing a [layer_spec]. 21 | #' The contained [layer_spec] stores information on how to construct the 22 | #' stat, which allows it to be combined with other objects (e.g. by piping it 23 | #' into `geom_()`) to construct more complex layers. See *Examples*. 24 | #' 25 | #' @returns 26 | #' A ggbuilder [layer], which is also a [ggplot2::layer], and which can be 27 | #' added to a [ggplot()] object or piped into other ggbuilder functions. 28 | #' 29 | #' @examples 30 | #' library(ggplot2) 31 | #' 32 | #' set.seed(123456) 33 | #' df = data.frame(condition = c("A", "B", "C"), response = round(rnorm(30, 1:3), 1)) 34 | #' 35 | #' df |> 36 | #' ggplot(aes(x = condition, y = response, color = condition)) + 37 | #' geom_boxplot() + 38 | #' stat_("boxplot", aes(y = response)) |> 39 | #' geom_("label", aes(y = ymax, label = ymax)) |> 40 | #' remap(aes(fill = scales::alpha(color, 0.1))) + 41 | #' theme_light() 42 | #' 43 | #' @import ggplot2 44 | #' @importFrom rlang expr 45 | #' @name stat_ 46 | NULL 47 | 48 | new_stat = function(stat, mapping = aes(), ..., .layer_spec = new_layer_spec()) { 49 | if (!is.null(.layer_spec$geom)) { 50 | stop0("Cannot apply a stat_() to a that already has a geom_()") 51 | } 52 | .layer_spec$stat = stat 53 | .layer_spec$mapping_stat = mapping 54 | .layer_spec$params = defaults(list(...), .layer_spec$params) 55 | new_layer(.layer_spec) 56 | } 57 | 58 | #' @export 59 | stat_ = function(x, ...) { 60 | UseMethod("stat_") 61 | } 62 | 63 | #' @export 64 | stat_.character = function(x, ...) { 65 | new_stat(stat = x, ...) 66 | } 67 | 68 | #' @export 69 | stat_.ggbuilder_layer = function(x, ...) { 70 | new_stat(..., .layer_spec = attr(x, "ggbuilder_layer_spec")) 71 | } 72 | 73 | #' @export 74 | stat_.ggbuilder_layer_spec = function(x, ...) { 75 | new_stat(..., .layer_spec = x) 76 | } 77 | 78 | #' @export 79 | stat_.ggbuilder_plot_data = function(x, ...) { 80 | new_stat(data = x$data, ...) 81 | } 82 | -------------------------------------------------------------------------------- /R/geom.R: -------------------------------------------------------------------------------- 1 | #' Pipable ggplot2 geoms 2 | #' 3 | #' A [ggplot2::layer()] containing a [ggplot2::Geom]; intended for piping. 4 | #' 5 | #' @param x One of: 6 | #' - A string: The suffix of a `geom_XXX` function used to create this geom. 7 | #' - A \pkg{ggbuilder} [layer], [layer_spec], or [plot_data] object representing 8 | #' earlier data and data transformations used as input to this geom. In this 9 | #' case, the second argument should be the suffix of a `geom_XXX` function 10 | #' used to create this geom. 11 | #' @param ... arguments passed on to the geom's constructor. 12 | #' 13 | #' @details 14 | #' `geom_()` can be used to replace typical `geom_XXX()` calls in \pkg{ggplot2} 15 | #' code to make them pipable by using the `XXX` suffix as a string as the first 16 | #' argument to `geom_()`. 17 | #' 18 | #' For example, `geom_boxplot(...)` would become `geom_("boxplot", ...)`. 19 | #' The primary difference between the former and the latter is that the latter 20 | #' is both a [ggplot2::layer] *and* a ggbuilder [layer] containing a [layer_spec]. 21 | #' The contained [layer_spec] stores information on how to construct the 22 | #' geom, which allows it to be combined with other objects (e.g. by piping it 23 | #' into `remap()`) to construct more complex layers. See *Examples*. 24 | #' 25 | #' @returns 26 | #' A ggbuilder [layer], which is also a [ggplot2::layer], and which can be 27 | #' added to a [ggplot()] object or piped into other ggbuilder functions. 28 | #' 29 | #' @examples 30 | #' library(ggplot2) 31 | #' 32 | #' set.seed(123456) 33 | #' df = data.frame(condition = c("A", "B", "C"), response = round(rnorm(30, 1:3), 1)) 34 | #' 35 | #' df |> 36 | #' ggplot(aes(x = condition, y = response, color = condition)) + 37 | #' geom_boxplot() + 38 | #' stat_("boxplot", aes(y = response)) |> 39 | #' geom_("label", aes(y = ymax, label = ymax)) |> 40 | #' remap(aes(fill = scales::alpha(color, 0.1))) + 41 | #' theme_light() 42 | #' 43 | #' @import ggplot2 44 | #' @importFrom rlang expr 45 | #' @name geom_ 46 | NULL 47 | 48 | new_geom = function(geom, mapping = aes(), ..., .layer_spec = new_layer_spec()) { 49 | .layer_spec$geom = geom 50 | if (is.null(.layer_spec$stat)) { 51 | # no stat has been supplied to the layer, so use the geom's default stat 52 | # and apply the provided mapping before the stat, not after it. 53 | .layer_spec$mapping_stat = mapping 54 | } else { 55 | .layer_spec$mapping_geom = mapping 56 | } 57 | .layer_spec$params = defaults(list(...), .layer_spec$params) 58 | new_layer(.layer_spec) 59 | } 60 | 61 | #' @rdname geom_ 62 | #' @export 63 | geom_ = function(x, ...) { 64 | UseMethod("geom_") 65 | } 66 | 67 | #' @rdname geom_ 68 | #' @export 69 | geom_.character = function(x, ...) { 70 | new_geom(geom = x, ...) 71 | } 72 | 73 | #' @rdname geom_ 74 | #' @export 75 | geom_.ggbuilder_layer = function(x, ...) { 76 | new_geom(..., .layer_spec = attr(x, "ggbuilder_layer_spec")) 77 | } 78 | 79 | #' @rdname geom_ 80 | #' @export 81 | geom_.ggbuilder_layer_spec = function(x, ...) { 82 | new_geom(..., .layer_spec = x) 83 | } 84 | 85 | #' @rdname geom_ 86 | #' @export 87 | geom_.ggbuilder_plot_data = function(x, ...) { 88 | new_geom(data = x$data, ...) 89 | } 90 | -------------------------------------------------------------------------------- /R/layer.R: -------------------------------------------------------------------------------- 1 | #' Pipable ggplot2 layers 2 | #' 3 | #' A [ggplot2::layer] built from a [ggbuilder::layer_spec] and intended for 4 | #' piping. Typically constructed using [geom_()] or [stat_()], not this function. 5 | #' 6 | #' @param x One of: 7 | #' - A \pkg{ggbuilder} [layer_spec] 8 | #' - A \pkg{ggbuilder} [layer] 9 | #' 10 | #' @returns 11 | #' 12 | #' A ggbuilder [layer], which is also a [ggplot2::layer], and which can be 13 | #' added to a [ggplot()] object or piped into other ggbuilder functions. 14 | #' 15 | #' @examples 16 | #' 17 | #' # See geom_() or stat_() for examples; this function 18 | #' # should not usually be used directly. 19 | #' 20 | #' @import ggplot2 21 | #' @importFrom rlang expr 22 | #' @name layer 23 | #' @export 24 | new_layer = function(x) { 25 | UseMethod("new_layer") 26 | } 27 | 28 | #' @rdname layer 29 | #' @export 30 | new_layer.ggbuilder_layer = function(x) { 31 | new_layer(attr(x, "ggbuilder_layer_spec")) 32 | } 33 | 34 | #' @rdname layer 35 | #' @export 36 | new_layer.ggbuilder_layer_spec = function(x) { 37 | params = x$params 38 | 39 | if (!is.null(x$geom)) { 40 | fun = paste0("geom_", x$geom) 41 | if (!is.null(x$stat)) { 42 | params$stat = x$stat 43 | } 44 | } else if (!is.null(x$stat)) { 45 | fun = paste0("stat_", x$stat) 46 | } else { 47 | stop0("Layers must have either a geom or a stat") 48 | } 49 | 50 | # translate aesthetic mappings into a single aesthetic mapping with 51 | # appropriate calls to stage(), after_scale(), or after_stat() 52 | stat_aes_names = names(x$mapping_stat) 53 | geom_aes_names = names(x$mapping_geom) 54 | rescale_aes_names = names(x$mapping_final) 55 | aes_names = union(union(stat_aes_names, geom_aes_names), rescale_aes_names) 56 | params$mapping = lapply(aes_names, function(aes_name) { 57 | has_start = aes_name %in% stat_aes_names 58 | has_after_stat = aes_name %in% geom_aes_names 59 | has_after_scale = aes_name %in% rescale_aes_names 60 | 61 | # stage(after_stat = xxx) is seemingly not equivalent to after_stat(xxx) 62 | # so we'll enumerate the different combinations to figure out the necessary 63 | # function to call, though this is kinda ugly 64 | if (!has_start) { # start | after_stat | after_scale | 65 | if (!has_after_stat) quo( 66 | after_scale(!!x$mapping_final[[aes_name]]) # | | XXXX | 67 | ) else if (has_after_scale) quo(stage( 68 | after_stat = !!x$mapping_geom[[aes_name]], # | XXXX | XXXX | 69 | after_scale = !!x$mapping_final[[aes_name]] 70 | )) else quo( 71 | after_stat(!!x$mapping_geom[[aes_name]]) # | XXXX | | 72 | ) 73 | } else if (has_after_stat) { 74 | if (has_after_scale) quo(stage( 75 | !!x$mapping_stat[[aes_name]], # XXXX | XXXX | XXXX | 76 | after_stat = !!x$mapping_geom[[aes_name]], 77 | after_scale = !!x$mapping_final[[aes_name]] 78 | )) 79 | else quo(stage( 80 | !!x$mapping_stat[[aes_name]], # XXXX | XXXX | | 81 | after_stat = !!x$mapping_geom[[aes_name]] 82 | )) 83 | } else if (has_after_scale) quo(stage( 84 | !!x$mapping_stat[[aes_name]], # XXXX | | XXXX | 85 | after_scale = !!x$mapping_final[[aes_name]] 86 | )) else quo( 87 | !!x$mapping_stat[[aes_name]] # XXXX | | | 88 | ) 89 | }) 90 | names(params$mapping) = aes_names 91 | class(params$mapping) = "uneval" 92 | 93 | .layer = do.call(fun, params) 94 | oldClass(.layer) = c("ggbuilder_layer", oldClass(.layer)) 95 | attr(.layer, "ggbuilder_layer_spec") = x 96 | .layer 97 | } 98 | 99 | 100 | # printing ---------------------------------------------------------------- 101 | 102 | #' @export 103 | print.ggbuilder_layer = function(x, ...) { 104 | cat0(":\n") 105 | NextMethod() 106 | cat0("\nfrom ") 107 | print(attr(x, "ggbuilder_layer_spec"), ...) 108 | invisible(x) 109 | } 110 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/layer/single-geom.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 0 64 | 10 65 | 20 66 | 30 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | A 75 | B 76 | C 77 | condition 78 | response_time 79 | single geom 80 | 81 | 82 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/layer/single-stat-and-geom.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 0 64 | 10 65 | 20 66 | 30 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | A 75 | B 76 | C 77 | condition 78 | response_time 79 | single stat and geom 80 | 81 | 82 | -------------------------------------------------------------------------------- /R/plot-data-verbs.R: -------------------------------------------------------------------------------- 1 | # Implementations of lazy dplyr / tidyr verbs for plot_data() 2 | 3 | 4 | # sequences of lazy operations on plot data ------------------------------- 5 | 6 | plot_data_sequence = function(x, fun, ...) { 7 | UseMethod("plot_data_sequence") 8 | } 9 | 10 | #' @export 11 | plot_data_sequence.NULL = function(x, fun, ...) { 12 | plot_data(function(d) fun(d, ...)) 13 | } 14 | 15 | #' @export 16 | plot_data_sequence.data.frame = function(x, fun, ...) { 17 | plot_data(fun(x, ...)) 18 | } 19 | 20 | #' @export 21 | plot_data_sequence.function = function(x, fun, ...) { 22 | plot_data(function(d) fun(x(d), ...)) 23 | } 24 | 25 | 26 | # dplyr verbs ------------------------------------------------------------- 27 | 28 | #' @exportS3Method dplyr::arrange ggbuilder_plot_data 29 | arrange.ggbuilder_plot_data = function(.data, ...) { 30 | plot_data_sequence(.data$data, dplyr::arrange, ...) 31 | } 32 | 33 | #' @exportS3Method dplyr::count ggbuilder_plot_data 34 | count.ggbuilder_plot_data = function(x, ...) { 35 | plot_data_sequence(x$data, dplyr::count, ...) 36 | } 37 | 38 | #' @exportS3Method dplyr::distinct ggbuilder_plot_data 39 | distinct.ggbuilder_plot_data = function(.data, ...) { 40 | plot_data_sequence(.data$data, dplyr::distinct, ...) 41 | } 42 | 43 | #' @exportS3Method dplyr::do ggbuilder_plot_data 44 | do.ggbuilder_plot_data = function(.data, ...) { 45 | plot_data_sequence(.data$data, dplyr::do, ...) 46 | } 47 | 48 | #' @exportS3Method dplyr::filter ggbuilder_plot_data 49 | filter.ggbuilder_plot_data = function(.data, ...) { 50 | plot_data_sequence(.data$data, dplyr::filter, ...) 51 | } 52 | 53 | #' @exportS3Method dplyr::group_by ggbuilder_plot_data 54 | group_by.ggbuilder_plot_data = function(.data, ...) { 55 | plot_data_sequence(.data$data, dplyr::group_by, ...) 56 | } 57 | 58 | #' @exportS3Method dplyr::inner_join ggbuilder_plot_data 59 | inner_join.ggbuilder_plot_data = function(x, ...) { 60 | plot_data_sequence(x$data, dplyr::inner_join, ...) 61 | } 62 | 63 | #' @exportS3Method dplyr::left_join ggbuilder_plot_data 64 | left_join.ggbuilder_plot_data = function(x, ...) { 65 | plot_data_sequence(x$data, dplyr::left_join, ...) 66 | } 67 | 68 | #' @exportS3Method dplyr::right_join ggbuilder_plot_data 69 | right_join.ggbuilder_plot_data = function(x, ...) { 70 | plot_data_sequence(x$data, dplyr::right_join, ...) 71 | } 72 | 73 | #' @exportS3Method dplyr::full_join ggbuilder_plot_data 74 | full_join.ggbuilder_plot_data = function(x, ...) { 75 | plot_data_sequence(x$data, dplyr::full_join, ...) 76 | } 77 | 78 | #' @exportS3Method dplyr::mutate ggbuilder_plot_data 79 | mutate.ggbuilder_plot_data = function(.data, ...) { 80 | plot_data_sequence(.data$data, dplyr::mutate, ...) 81 | } 82 | 83 | #' @exportS3Method dplyr::transmute ggbuilder_plot_data 84 | transmute.ggbuilder_plot_data = function(.data, ...) { 85 | plot_data_sequence(.data$data, dplyr::transmute, ...) 86 | } 87 | 88 | #' @exportS3Method dplyr::pull ggbuilder_plot_data 89 | pull.ggbuilder_plot_data = function(.data, ...) { 90 | plot_data_sequence(.data$data, dplyr::pull, ...) 91 | } 92 | 93 | #' @exportS3Method dplyr::select ggbuilder_plot_data 94 | select.ggbuilder_plot_data = function(.data, ...) { 95 | plot_data_sequence(.data$data, dplyr::select, ...) 96 | } 97 | 98 | #' @exportS3Method dplyr::slice ggbuilder_plot_data 99 | slice.ggbuilder_plot_data = function(.data, ...) { 100 | plot_data_sequence(.data$data, dplyr::slice, ...) 101 | } 102 | 103 | #' @exportS3Method dplyr::slice_head ggbuilder_plot_data 104 | slice_head.ggbuilder_plot_data = function(.data, ...) { 105 | plot_data_sequence(.data$data, dplyr::slice_head, ...) 106 | } 107 | 108 | #' @exportS3Method dplyr::slice_tail ggbuilder_plot_data 109 | slice_tail.ggbuilder_plot_data = function(.data, ...) { 110 | plot_data_sequence(.data$data, dplyr::slice_tail, ...) 111 | } 112 | 113 | #' @exportS3Method dplyr::slice_sample ggbuilder_plot_data 114 | slice_sample.ggbuilder_plot_data = function(.data, ...) { 115 | plot_data_sequence(.data$data, dplyr::slice_sample, ...) 116 | } 117 | 118 | #' @exportS3Method dplyr::slice_min ggbuilder_plot_data 119 | slice_min.ggbuilder_plot_data = function(.data, ...) { 120 | plot_data_sequence(.data$data, dplyr::slice_min, ...) 121 | } 122 | 123 | #' @exportS3Method dplyr::slice_max ggbuilder_plot_data 124 | slice_max.ggbuilder_plot_data = function(.data, ...) { 125 | plot_data_sequence(.data$data, dplyr::slice_max, ...) 126 | } 127 | 128 | #' @exportS3Method dplyr::summarise ggbuilder_plot_data 129 | summarise.ggbuilder_plot_data = function(.data, ...) { 130 | plot_data_sequence(.data$data, dplyr::summarise, ...) 131 | } 132 | 133 | #' @exportS3Method dplyr::summarize ggbuilder_plot_data 134 | summarize.ggbuilder_plot_data = function(.data, ...) { 135 | plot_data_sequence(.data$data, dplyr::summarize, ...) 136 | } 137 | 138 | 139 | # generics verbs ---------------------------------------------------------- 140 | 141 | #' @exportS3Method generics::intersect ggbuilder_plot_data 142 | intersect.ggbuilder_plot_data = function(x, ...) { 143 | plot_data_sequence(x$data, generics::intersect, ...) 144 | } 145 | 146 | #' @exportS3Method generics::union ggbuilder_plot_data 147 | union.ggbuilder_plot_data = function(x, ...) { 148 | plot_data_sequence(x$data, generics::union, ...) 149 | } 150 | 151 | #' @exportS3Method generics::setdiff ggbuilder_plot_data 152 | setdiff.ggbuilder_plot_data = function(x, ...) { 153 | plot_data_sequence(x$data, generics::setdiff, ...) 154 | } 155 | 156 | 157 | # tidyr verbs ------------------------------------------------------------- 158 | 159 | #' @exportS3Method tidyr::expand ggbuilder_plot_data 160 | expand.ggbuilder_plot_data = function(data, ...) { 161 | plot_data_sequence(data$data, tidyr::expand, ...) 162 | } 163 | 164 | #' @exportS3Method tidyr::fill ggbuilder_plot_data 165 | fill.ggbuilder_plot_data = function(data, ...) { 166 | plot_data_sequence(data$data, tidyr::fill, ...) 167 | } 168 | 169 | #' @exportS3Method tidyr::pivot_longer ggbuilder_plot_data 170 | pivot_longer.ggbuilder_plot_data = function(data, ...) { 171 | plot_data_sequence(data$data, tidyr::pivot_longer, ...) 172 | } 173 | 174 | #' @exportS3Method tidyr::pivot_wider ggbuilder_plot_data 175 | pivot_wider.ggbuilder_plot_data = function(data, ...) { 176 | plot_data_sequence(data$data, tidyr::pivot_wider, ...) 177 | } 178 | 179 | 180 | # base verbs -------------------------------------------------------------- 181 | 182 | #' @exportS3Method utils::head ggbuilder_plot_data 183 | head.ggbuilder_plot_data = function(x, ...) { 184 | plot_data_sequence(x$data, utils::head, ...) 185 | } 186 | 187 | #' @exportS3Method utils::tail ggbuilder_plot_data 188 | tail.ggbuilder_plot_data = function(x, ...) { 189 | plot_data_sequence(x$data, utils::tail, ...) 190 | } 191 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | --- 4 | 5 | 6 | 7 | [![R-CMD-check](https://github.com/mjskay/ggbuilder/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/mjskay/ggbuilder/actions/workflows/R-CMD-check.yaml) 8 | [![Codecov test coverage](https://codecov.io/gh/mjskay/ggbuilder/branch/main/graph/badge.svg)](https://app.codecov.io/gh/mjskay/ggbuilder?branch=main) 9 | 10 | 11 | ```{r, include = FALSE} 12 | knitr::opts_chunk$set( 13 | collapse = TRUE, 14 | comment = "#>", 15 | fig.path = "man/figures/README-", 16 | out.width = "100%", 17 | fig.retina = 2 18 | ) 19 | ``` 20 | 21 | # ggbuilder: A pipe-like approach to building individual ggplot layers 22 | 23 | {ggbuilder} is *experimental* alternative approach to specifying individual [ggplot2](https://ggplot2.tidyverse.org/) 24 | layers. It was inspired by an observation made by June Choe in his [RStudio::conf 2022 talk](https://twitter.com/yjunechoe/status/1552658547867017217?s=20&t=eC4R9YVtf51Fa8k_GTySUw): 25 | some elements of a layer specification (i.e. calls to `geom_XXX()` or `stat_XXX()`) 26 | tend to be written out-of-order compared to how those elements are actually executed by ggplot2, 27 | which may make it hard to reason about. 28 | {ggbuilder} provides an alternative syntax for specifying layers that mimics 29 | familiar data transformation pipelines, and attempts to put operations in the 30 | same order syntactically as they are actually executed. 31 | 32 | ## Installation 33 | 34 | You can install the development version of ggbuilder from [GitHub](https://github.com/) with: 35 | 36 | ``` r 37 | # install.packages("devtools") 38 | devtools::install_github("mjskay/ggbuilder") 39 | ``` 40 | 41 | ## Example 42 | 43 | Here is a simple dataset, visuallized using normal ggplot2 syntax: 44 | 45 | ```{r dataset, message=FALSE} 46 | library(ggplot2) 47 | library(ggbuilder) 48 | library(dplyr) 49 | theme_set(theme_light()) 50 | 51 | set.seed(123456) 52 | df = data.frame(condition = c("A", "B", "C"), response = round(rnorm(30, 1:3), 1)) 53 | df |> 54 | ggplot(aes(x = condition, y = response, color = condition)) + 55 | geom_point() 56 | ``` 57 | 58 | Or you might try use a boxplot instead (though personally I would probably use 59 | something from [ggdist](https://mjskay.github.io/ggdist/) instead ;) ) 60 | 61 | ```{r boxplot_ggplot} 62 | df |> 63 | ggplot(aes(x = condition, y = response, color = condition)) + 64 | geom_boxplot(size = 0.75) 65 | ``` 66 | 67 | To translate the above to {ggbuilder}, we can start by replacing calls of the 68 | form `geom_XXX(...)` or `stat_XXX(...)` with `geom_("XXX", ...)` or `stat_("XXX", ...)`. 69 | These will work just as before: 70 | 71 | ```{r boxplot_ggbuilder} 72 | df |> 73 | ggplot(aes(x = condition, y = response, color = condition)) + 74 | geom_("boxplot", size = 0.75) 75 | ``` 76 | 77 | It works just as before, except that the objects created by calls to `geom_()` 78 | and `stat_()` are both traditional ggplot2 layers *and* ggbuilder layer *specifications*, 79 | which can be chained together using the pipe: `|>`. 80 | 81 | ```{r} 82 | geom_("boxplot", size = 0.75) 83 | ``` 84 | 85 | This allows us to replicate an example from June Choe's talk in which the boxplot 86 | stat is reused with a label geometry instead of its normal boxplot geometry. 87 | 88 | **In ggplot2**, this requires using `after_stat()` to reassign the y aesthetic 89 | after `stat_boxplot()` does its computation: 90 | 91 | ```{r boxplot_label_1} 92 | df |> 93 | ggplot(aes(x = condition, y = response, color = condition)) + 94 | geom_boxplot() + 95 | geom_label( 96 | stat = "boxplot", 97 | aes(y = stage(response, after_stat = ymax), label = after_stat(ymax)) 98 | ) 99 | ``` 100 | 101 | This requires using the `ggplot2::stage()` and `ggplot2::after_stat()` functions. 102 | As June Choe pointed out in his ggtrace talk, the flow from plot data to stat 103 | transformation to geom aesthetics is written somewhat out-of-order inside the call 104 | to `aes()`. 105 | 106 | **In ggbuilder**, we can write this data flow in order by piping a `stat_()` into 107 | a `geom_()`, and specifying the aesthetics associated with each: 108 | 109 | ```{r boxplot_label_2} 110 | df |> 111 | ggplot(aes(x = condition, y = response, color = condition)) + 112 | geom_boxplot() + 113 | stat_("boxplot", aes(y = response)) |> 114 | geom_("label", aes(y = ymax, label = ymax)) 115 | ``` 116 | 117 | Under the hood, {ggbuilder} translates this into the appropriate calls to 118 | `stage()` in order to build the geom. 119 | 120 | ## Changes to aesthetics after scaling 121 | 122 | {ggbuilder} also allows us to do the equivalent of `ggplot2::after_scale()` by 123 | piping a layer specification into `remap()`. This will apply changes to 124 | aesthetics after scales have been applied: 125 | 126 | ```{r remap} 127 | df |> 128 | ggplot(aes(x = condition, y = response, color = condition)) + 129 | geom_boxplot() + 130 | stat_("boxplot", aes(y = response)) |> 131 | geom_("label", aes(y = ymax, label = ymax)) |> 132 | remap(aes(fill = colorspace::lighten(color, 0.9))) 133 | ``` 134 | 135 | ## Changes to data 136 | 137 | We can also modify the input data by passing a new data frame (or a function that 138 | modifies a data frame) to the `plot_data()` function at the top of a layer specification 139 | pipe: 140 | 141 | ```{r data_1} 142 | df |> 143 | ggplot(aes(x = condition, y = response, color = condition)) + 144 | geom_boxplot() + 145 | plot_data(\(x) filter(x, condition %in% c("B", "C"))) |> 146 | stat_("boxplot", aes(y = response)) |> 147 | geom_("label", aes(y = ymax, label = ymax)) |> 148 | remap(aes(fill = colorspace::lighten(color, 0.9))) 149 | ``` 150 | 151 | {ggbuilder} also provides implementations of {dplyr} and {tidyr} verbs for `plot_data()`, 152 | so you can pipe `plot_data()` into those functions instead of awkwardly passing them 153 | as an argument wrapped in an anonymous function (like above): 154 | 155 | ```{r data_2} 156 | df |> 157 | ggplot(aes(x = condition, y = response, color = condition)) + 158 | geom_boxplot() + 159 | plot_data() |> 160 | filter(condition %in% c("B", "C")) |> 161 | stat_("boxplot", aes(y = response)) |> 162 | geom_("label", aes(y = ymax, label = ymax)) |> 163 | remap(aes(fill = colorspace::lighten(color, 0.9))) 164 | ``` 165 | 166 | In fact, we could do the transformations ourselves, 167 | which may more clearly communiate our intent: 168 | 169 | ```{r data_3} 170 | df |> 171 | ggplot(aes(x = condition, y = response, color = condition)) + 172 | geom_boxplot() + 173 | plot_data() |> 174 | filter(condition %in% c("B", "C")) |> 175 | group_by(condition) |> 176 | slice_max(response) |> 177 | geom_("label", aes(label = response)) |> 178 | remap(aes(fill = colorspace::lighten(color, 0.9))) 179 | ``` 180 | 181 | ## Feedback 182 | 183 | This package is **very** experimental! Feedback/issues are welcome. Not sure 184 | if this will ever go CRAN-wards, but maybe if it solidifies in the future :). 185 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | [![R-CMD-check](https://github.com/mjskay/ggbuilder/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/mjskay/ggbuilder/actions/workflows/R-CMD-check.yaml) 6 | [![Codecov test 7 | coverage](https://codecov.io/gh/mjskay/ggbuilder/branch/main/graph/badge.svg)](https://app.codecov.io/gh/mjskay/ggbuilder?branch=main) 8 | 9 | 10 | # ggbuilder: A pipe-like approach to building individual ggplot layers 11 | 12 | {ggbuilder} is *experimental* alternative approach to specifying 13 | individual [ggplot2](https://ggplot2.tidyverse.org/) layers. It was 14 | inspired by an observation made by June Choe in his [RStudio::conf 2022 15 | talk](https://twitter.com/yjunechoe/status/1552658547867017217?s=20&t=eC4R9YVtf51Fa8k_GTySUw): 16 | some elements of a layer specification (i.e. calls to `geom_XXX()` or 17 | `stat_XXX()`) tend to be written out-of-order compared to how those 18 | elements are actually executed by ggplot2, which may make it hard to 19 | reason about. {ggbuilder} provides an alternative syntax for specifying 20 | layers that mimics familiar data transformation pipelines, and attempts 21 | to put operations in the same order syntactically as they are actually 22 | executed. 23 | 24 | ## Installation 25 | 26 | You can install the development version of ggbuilder from 27 | [GitHub](https://github.com/) with: 28 | 29 | ``` r 30 | # install.packages("devtools") 31 | devtools::install_github("mjskay/ggbuilder") 32 | ``` 33 | 34 | ## Example 35 | 36 | Here is a simple dataset, visuallized using normal ggplot2 syntax: 37 | 38 | ``` r 39 | library(ggplot2) 40 | library(ggbuilder) 41 | library(dplyr) 42 | theme_set(theme_light()) 43 | 44 | set.seed(123456) 45 | df = data.frame(condition = c("A", "B", "C"), response = round(rnorm(30, 1:3), 1)) 46 | df |> 47 | ggplot(aes(x = condition, y = response, color = condition)) + 48 | geom_point() 49 | ``` 50 | 51 | 52 | 53 | Or you might try use a boxplot instead (though personally I would 54 | probably use something from [ggdist](https://mjskay.github.io/ggdist/) 55 | instead ;) ) 56 | 57 | ``` r 58 | df |> 59 | ggplot(aes(x = condition, y = response, color = condition)) + 60 | geom_boxplot(size = 0.75) 61 | ``` 62 | 63 | 64 | 65 | To translate the above to {ggbuilder}, we can start by replacing calls 66 | of the form `geom_XXX(...)` or `stat_XXX(...)` with `geom_("XXX", ...)` 67 | or `stat_("XXX", ...)`. These will work just as before: 68 | 69 | ``` r 70 | df |> 71 | ggplot(aes(x = condition, y = response, color = condition)) + 72 | geom_("boxplot", size = 0.75) 73 | ``` 74 | 75 | 76 | 77 | It works just as before, except that the objects created by calls to 78 | `geom_()` and `stat_()` are both traditional ggplot2 layers *and* 79 | ggbuilder layer *specifications*, which can be chained together using 80 | the pipe: `|>`. 81 | 82 | ``` r 83 | geom_("boxplot", size = 0.75) 84 | #> : 85 | #> mapping: 86 | #> geom_boxplot: outlier.colour = NULL, outlier.fill = NULL, outlier.shape = 19, outlier.size = 1.5, outlier.stroke = 0.5, outlier.alpha = NULL, notch = FALSE, notchwidth = 0.5, varwidth = FALSE, na.rm = FALSE, orientation = NA 87 | #> stat_boxplot: na.rm = FALSE, orientation = NA 88 | #> position_dodge2 89 | #> 90 | #> from : 91 | #> $params 92 | #> $params$size 93 | #> [1] 0.75 94 | #> 95 | #> 96 | #> $mapping_stat 97 | #> Aesthetic mapping: 98 | #> 99 | #> 100 | #> $mapping_geom 101 | #> Aesthetic mapping: 102 | #> 103 | #> 104 | #> $mapping_final 105 | #> Aesthetic mapping: 106 | #> 107 | #> 108 | #> $geom 109 | #> [1] "boxplot" 110 | ``` 111 | 112 | This allows us to replicate an example from June Choe’s talk in which 113 | the boxplot stat is reused with a label geometry instead of its normal 114 | boxplot geometry. 115 | 116 | **In ggplot2**, this requires using `after_stat()` to reassign the y 117 | aesthetic after `stat_boxplot()` does its computation: 118 | 119 | ``` r 120 | df |> 121 | ggplot(aes(x = condition, y = response, color = condition)) + 122 | geom_boxplot() + 123 | geom_label( 124 | stat = "boxplot", 125 | aes(y = stage(response, after_stat = ymax), label = after_stat(ymax)) 126 | ) 127 | ``` 128 | 129 | 130 | 131 | This requires using the `ggplot2::stage()` and `ggplot2::after_stat()` 132 | functions. As June Choe pointed out in his ggtrace talk, the flow from 133 | plot data to stat transformation to geom aesthetics is written somewhat 134 | out-of-order inside the call to `aes()`. 135 | 136 | **In ggbuilder**, we can write this data flow in order by piping a 137 | `stat_()` into a `geom_()`, and specifying the aesthetics associated 138 | with each: 139 | 140 | ``` r 141 | df |> 142 | ggplot(aes(x = condition, y = response, color = condition)) + 143 | geom_boxplot() + 144 | stat_("boxplot", aes(y = response)) |> 145 | geom_("label", aes(y = ymax, label = ymax)) 146 | ``` 147 | 148 | 149 | 150 | Under the hood, {ggbuilder} translates this into the appropriate calls 151 | to `stage()` in order to build the geom. 152 | 153 | ## Changes to aesthetics after scaling 154 | 155 | {ggbuilder} also allows us to do the equivalent of 156 | `ggplot2::after_scale()` by piping a layer specification into `remap()`. 157 | This will apply changes to aesthetics after scales have been applied: 158 | 159 | ``` r 160 | df |> 161 | ggplot(aes(x = condition, y = response, color = condition)) + 162 | geom_boxplot() + 163 | stat_("boxplot", aes(y = response)) |> 164 | geom_("label", aes(y = ymax, label = ymax)) |> 165 | remap(aes(fill = colorspace::lighten(color, 0.9))) 166 | #> Warning: Duplicated aesthetics after name standardisation: NA 167 | #> Duplicated aesthetics after name standardisation: NA 168 | ``` 169 | 170 | 171 | 172 | ## Changes to data 173 | 174 | We can also modify the input data by passing a new data frame (or a 175 | function that modifies a data frame) to the `plot_data()` function at 176 | the top of a layer specification pipe: 177 | 178 | ``` r 179 | df |> 180 | ggplot(aes(x = condition, y = response, color = condition)) + 181 | geom_boxplot() + 182 | plot_data(\(x) filter(x, condition %in% c("B", "C"))) |> 183 | stat_("boxplot", aes(y = response)) |> 184 | geom_("label", aes(y = ymax, label = ymax)) |> 185 | remap(aes(fill = colorspace::lighten(color, 0.9))) 186 | #> Warning: Duplicated aesthetics after name standardisation: NA 187 | #> Duplicated aesthetics after name standardisation: NA 188 | ``` 189 | 190 | 191 | 192 | {ggbuilder} also provides implementations of {dplyr} and {tidyr} verbs 193 | for `plot_data()`, so you can pipe `plot_data()` into those functions 194 | instead of awkwardly passing them as an argument wrapped in an anonymous 195 | function (like above): 196 | 197 | ``` r 198 | df |> 199 | ggplot(aes(x = condition, y = response, color = condition)) + 200 | geom_boxplot() + 201 | plot_data() |> 202 | filter(condition %in% c("B", "C")) |> 203 | stat_("boxplot", aes(y = response)) |> 204 | geom_("label", aes(y = ymax, label = ymax)) |> 205 | remap(aes(fill = colorspace::lighten(color, 0.9))) 206 | #> Warning: Duplicated aesthetics after name standardisation: NA 207 | #> Duplicated aesthetics after name standardisation: NA 208 | ``` 209 | 210 | 211 | 212 | In fact, we could do the transformations ourselves, which may more 213 | clearly communiate our intent: 214 | 215 | ``` r 216 | df |> 217 | ggplot(aes(x = condition, y = response, color = condition)) + 218 | geom_boxplot() + 219 | plot_data() |> 220 | filter(condition %in% c("B", "C")) |> 221 | group_by(condition) |> 222 | slice_max(response) |> 223 | geom_("label", aes(label = response)) |> 224 | remap(aes(fill = colorspace::lighten(color, 0.9))) 225 | ``` 226 | 227 | 228 | 229 | ## Feedback 230 | 231 | This package is **very** experimental! Feedback/issues are welcome. Not 232 | sure if this will ever go CRAN-wards, but maybe if it solidifies in the 233 | future :). 234 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/layer/plot-data-with-several-verbs-and-remap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 29 44 | 45 | 30 46 | 47 | 48 | 49 | 0 50 | 10 51 | 20 52 | 30 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | A 61 | B 62 | C 63 | condition 64 | response_time 65 | 66 | condition 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | A 83 | B 84 | C 85 | plot_data() with several verbs and remap() 86 | 87 | 88 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/layer/plot-data-with-a-verb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 29 44 | 45 | 30 46 | 47 | 48 | 49 | 0 50 | 10 51 | 20 52 | 30 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | A 61 | B 62 | C 63 | condition 64 | response_time 65 | 66 | condition 67 | 68 | 69 | 70 | 71 | 72 | 73 | a 74 | 75 | 76 | 77 | 78 | 79 | 80 | a 81 | 82 | 83 | 84 | 85 | 86 | 87 | a 88 | A 89 | B 90 | C 91 | plot_data() with a verb 92 | 93 | 94 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/layer/plot-data-with-a-function.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 29 44 | 45 | 30 46 | 47 | 48 | 49 | 0 50 | 10 51 | 20 52 | 30 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | A 61 | B 62 | C 63 | condition 64 | response_time 65 | 66 | condition 67 | 68 | 69 | 70 | 71 | 72 | 73 | a 74 | 75 | 76 | 77 | 78 | 79 | 80 | a 81 | 82 | 83 | 84 | 85 | 86 | 87 | a 88 | A 89 | B 90 | C 91 | plot_data() with a function 92 | 93 | 94 | --------------------------------------------------------------------------------