├── .Rbuildignore ├── .github ├── .gitignore ├── ISSUE_TEMPLATE │ └── issue_template.md └── workflows │ ├── R-CMD-check.yaml │ ├── pkgdown.yaml │ └── test-coverage.yaml ├── .gitignore ├── DESCRIPTION ├── LICENSE ├── LICENSE.md ├── NAMESPACE ├── NEWS.md ├── R ├── class-layer-list.R ├── class-operation-.R ├── class-operation-adjust.R ├── class-operation-affine-transform.R ├── class-operation-blend.R ├── class-operation-composition.R ├── class-operation-nop.R ├── class-operation-product.R ├── class-operation-sum.R ├── ggblend-package.R ├── layer-.R ├── layer-list.R ├── operation-.R ├── operation-adjust.R ├── operation-affine-transform.R ├── operation-blend.R ├── operation-composition.R ├── operation-copy.R ├── operation-nop.R ├── operation-partition.R ├── operation-product.R ├── operation-sum.R ├── transform.R └── util.R ├── README.Rmd ├── README.md ├── _pkgdown.yml ├── codecov.yml ├── cran-comments.md ├── figures-source ├── ggplot2-ext-gallery.pdf ├── ggplot2-ext-gallery.png └── logo.pdf ├── ggblend.Rproj ├── inst ├── CITATION └── WORDLIST ├── man-roxygen └── operation.R ├── man ├── adjust.Rd ├── affine_transform.Rd ├── blend.Rd ├── copy.Rd ├── figures │ ├── README-gapminder-1.gif │ ├── README-lineribbon_blend-1.png │ ├── README-lineribbon_blend_highlight-1.png │ ├── README-lineribbon_noblend-1.png │ ├── README-scatter_blend-1.png │ ├── README-scatter_blend_geom-1.png │ ├── README-scatter_blend_geom_incorrect-1.png │ ├── README-scatter_lighten_multiply-1.png │ ├── README-scatter_lighten_multiply_stacked-1.png │ ├── README-scatter_noblend-1.png │ ├── README-scatter_partition_blend-1.png │ └── logo.svg ├── ggblend-package.Rd ├── layer-like.Rd ├── layer_list.Rd ├── nop.Rd ├── operation-class.Rd ├── operation_composition.Rd ├── operation_product.Rd ├── operation_sum.Rd └── partition.Rd ├── pkgdown ├── extra.scss └── favicon │ ├── apple-touch-icon-120x120.png │ ├── apple-touch-icon-152x152.png │ ├── apple-touch-icon-180x180.png │ ├── apple-touch-icon-60x60.png │ ├── apple-touch-icon-76x76.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ └── favicon.ico └── tests ├── testthat.R └── testthat ├── _snaps ├── operation- │ └── ggnewscale_with_blend.png ├── operation-affine-transform │ ├── affine_transform.png │ └── affine_transform_on_two_layers.png └── operation-blend │ ├── complex_blend_sequence.png │ ├── multiply_blend.png │ ├── multiply_blend_with_partition.png │ └── multiply_blend_without_partition.png ├── helper-expect-snapshot-plot.R ├── helper-grob.R ├── helper-layer.R ├── helper-warnings.R ├── test-layer-list.R ├── test-operation-.R ├── test-operation-adjust.R ├── test-operation-affine-transform.R ├── test-operation-blend.R ├── test-operation-composition.R ├── test-operation-copy.R ├── test-operation-nop.R ├── test-operation-partition.R ├── test-operation-product.R └── test-operation-sum.R /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^ggblend\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^LICENSE\.md$ 4 | ^README\.Rmd$ 5 | ^cran-comments\.md$ 6 | ^\.github$ 7 | ^codecov\.yml$ 8 | ^_pkgdown\.yml$ 9 | ^man-roxygen$ 10 | ^pkgdown$ 11 | ^docs$ 12 | ^figures-source$ 13 | ^CRAN-SUBMISSION$ 14 | -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report or feature request 3 | about: Describe a bug you've seen or make a case for a new feature 4 | --- 5 | 6 | Please briefly describe your problem and what output you expect. 7 | 8 | Please include a minimal reproducible example (AKA a reprex). If you've never heard of a [reprex](http://reprex.tidyverse.org/) before, start by reading . 9 | 10 | Brief description of the problem 11 | 12 | ```r 13 | # insert reprex here 14 | ``` 15 | -------------------------------------------------------------------------------- /.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: '*' 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@v3 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 | -------------------------------------------------------------------------------- /.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 | permissions: 23 | contents: write 24 | steps: 25 | - uses: actions/checkout@v3 26 | 27 | - uses: r-lib/actions/setup-pandoc@v2 28 | 29 | - uses: r-lib/actions/setup-r@v2 30 | with: 31 | use-public-rspm: true 32 | 33 | - uses: r-lib/actions/setup-r-dependencies@v2 34 | with: 35 | extra-packages: any::pkgdown, local::. 36 | needs: website 37 | 38 | - name: Build site 39 | run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) 40 | shell: Rscript {0} 41 | 42 | - name: Deploy to GitHub pages 🚀 43 | if: github.event_name != 'pull_request' 44 | uses: JamesIves/github-pages-deploy-action@v4.4.1 45 | with: 46 | clean: false 47 | branch: gh-pages 48 | folder: docs 49 | -------------------------------------------------------------------------------- /.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: '*' 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@v3 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: | 31 | covr::codecov( 32 | quiet = FALSE, 33 | clean = FALSE, 34 | install_path = file.path(Sys.getenv("RUNNER_TEMP"), "package") 35 | ) 36 | shell: Rscript {0} 37 | 38 | - name: Show testthat output 39 | if: always() 40 | run: | 41 | ## -------------------------------------------------------------------- 42 | find ${{ runner.temp }}/package -name 'testthat.Rout*' -exec cat '{}' \; || true 43 | shell: bash 44 | 45 | - name: Upload test results 46 | if: failure() 47 | uses: actions/upload-artifact@v3 48 | with: 49 | name: coverage-test-failures 50 | path: ${{ runner.temp }}/package 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .Rdata 4 | .httr-oauth 5 | .DS_Store 6 | *~ 7 | docs 8 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: ggblend 2 | Title: Blending and Compositing Algebra for 'ggplot2' 3 | Version: 0.1.0 4 | Date: 2023-05-23 5 | Authors@R: 6 | person("Matthew", "Kay", , "mjskay@northwestern.edu", role = c("aut", "cre", "cph"), 7 | comment = c(ORCID = "0000-0001-9446-0419")) 8 | Description: Algebra of operations for blending, copying, adjusting, and 9 | compositing layers in 'ggplot2'. Supports copying and adjusting the 10 | aesthetics or parameters of an existing layer, partitioning a layer 11 | into multiple pieces for re-composition, applying affine transformations 12 | to layers, and combining layers (or partitions of layers) using blend modes 13 | (including commutative blend modes, like multiply and darken). Blend 14 | mode support is particularly useful for creating plots with overlapping 15 | groups where the layer drawing order does not change the output; 16 | see Kindlmann and Scheidegger (2014) . 17 | License: MIT + file LICENSE 18 | Language: en-US 19 | Depends: R (>= 4.2) 20 | Imports: methods, grid, ggplot2 (>= 3.4.0), rlang 21 | Suggests: 22 | covr, 23 | testthat (>= 3.0.0), 24 | fontquiver, 25 | showtext, 26 | sysfonts, 27 | ggnewscale, 28 | scales 29 | Config/testthat/edition: 3 30 | BugReports: https://github.com/mjskay/ggblend/issues/new 31 | URL: https://mjskay.github.io/ggblend/, https://github.com/mjskay/ggblend/ 32 | Encoding: UTF-8 33 | Roxygen: list(markdown = TRUE) 34 | RoxygenNote: 7.2.3 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2023 2 | COPYRIGHT HOLDER: ggblend authors 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2023 ggblend 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 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method(as_layer_like,LayerInstance) 4 | S3method(as_layer_like,default) 5 | S3method(as_layer_like,layer_list) 6 | S3method(as_layer_like,list) 7 | S3method(as_layer_list,LayerInstance) 8 | S3method(as_layer_list,layer_list) 9 | S3method(as_layer_list,list) 10 | S3method(layer_apply,LayerInstance) 11 | S3method(layer_apply,list) 12 | export(adjust) 13 | export(affine_transform) 14 | export(as_layer_like) 15 | export(as_layer_list) 16 | export(blend) 17 | export(copy_over) 18 | export(copy_under) 19 | export(is_layer_like) 20 | export(layer_list) 21 | export(nop) 22 | export(partition) 23 | exportClasses(adjust) 24 | exportClasses(affine_transform) 25 | exportClasses(blend) 26 | exportClasses(layer_list) 27 | exportClasses(nop) 28 | exportClasses(operation) 29 | exportClasses(operation_composition) 30 | exportClasses(operation_product) 31 | exportClasses(operation_sum) 32 | exportMethods("*") 33 | exportMethods("+") 34 | exportMethods(format) 35 | exportMethods(prod) 36 | exportMethods(show) 37 | exportMethods(sum) 38 | import(ggplot2) 39 | import(grid) 40 | import(methods) 41 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # ggblend 0.1.0 2 | 3 | * Initial release. 4 | -------------------------------------------------------------------------------- /R/class-layer-list.R: -------------------------------------------------------------------------------- 1 | #' Lists of layer-like objects 2 | #' 3 | #' A list of [layer-like] objects, which can be used in layer [operation]s 4 | #' (through function application or multiplication) or added to a [ggplot2()] 5 | #' object. 6 | #' 7 | #' @param x,... [layer-like] objects 8 | #' @param object,e1,e2 [layer_list()]s 9 | #' 10 | #' @details 11 | #' For the most part, users of \pkg{ggblend} need not worry about this class. 12 | #' It is used internally to simplify multiple dispatch on binary operators, as 13 | #' the alternative ([list()]s of [ggplot2::layer()]s) is more cumbersome. 14 | #' \pkg{ggblend} converts input lists to this format as needed. 15 | #' 16 | #' @returns 17 | #' An object of class `"layer_list"`. 18 | #' 19 | #' @examples 20 | #' library(ggplot2) 21 | #' 22 | #' # layer_list()s act just like list()s of layer()s in that they can 23 | #' # be added to ggplot() objects 24 | #' data.frame(x = 1:10) |> 25 | #' ggplot(aes(x, x)) + 26 | #' layer_list( 27 | #' geom_line(), 28 | #' geom_point() 29 | #' ) 30 | #' 31 | #' @name layer_list 32 | #' @aliases layer_list-class 33 | #' @export 34 | setClass("layer_list", contains = "list") 35 | -------------------------------------------------------------------------------- /R/class-operation-.R: -------------------------------------------------------------------------------- 1 | #' Layer operations 2 | #' 3 | #' Layer [operation]s are composable transformations that can be applied to \pkg{ggplot2} 4 | #' [layer-like] objects, such as `stat`s, `geom`s, and lists of `stat`s and 5 | #' `geom`s; see the [layer-like] documentation page for a description of valid 6 | #' [layer-like] objects. 7 | #' 8 | #' @param x,object An [operation]. 9 | #' @param ... Further arguments passed to other methods. 10 | #' 11 | #' @details 12 | #' 13 | #' [operation]s can be composed using the `+` and `*` operators (see [operation_sum] 14 | #' and [operation_product]). Addition and multiplication of [operation]s and [layer-like] 15 | #' objects obeys the distributive law. 16 | #' 17 | #' [operation]s can be applied to [layer-like] objects using `*` or `|>`, with slightly 18 | #' different results: 19 | #' 20 | #' - Using `*`, application of [operation]s to a list of [layer-like] objects *is* distributive. For example, 21 | #' `list(geom_line(), geom_point()) * blend("multiply")` is 22 | #' equivalent to `list(geom_line() * blend("multiply"), geom_point() * blend("multiply"))`; 23 | #' i.e. it multiply-blends the contents of the two layers individually. 24 | #' 25 | #' - Using `|>`, application of [operation]s to a list of [layer-like] objects is *not* 26 | #' distributive (unless the only reasonable interpretation of applying the 27 | #' transformation is necessarily distributive; e.g. `adjust()`). For example, 28 | #' `list(geom_line(), geom_point()) |> blend("multiply")` would multiply-blend 29 | #' both layers together, rather than multiply-blending the contents of the 30 | #' two layers individually. 31 | #' 32 | #' @examples 33 | #' library(ggplot2) 34 | #' 35 | #' # operations can stand alone 36 | #' adjust(aes(color = x)) 37 | #' 38 | #' # they can also be applied to layers through multiplication or piping 39 | #' geom_line() |> adjust(aes(color = x)) 40 | #' geom_line() * adjust(aes(color = x)) 41 | #' 42 | #' # layer operations act as a small algebra, and can be combined through 43 | #' # multiplication and addition 44 | #' (adjust(fill = "green") + 1) * blend("multiply") 45 | #' 46 | #' @name operation-class 47 | #' @aliases operation 48 | #' @export 49 | setClass("operation") 50 | -------------------------------------------------------------------------------- /R/class-operation-adjust.R: -------------------------------------------------------------------------------- 1 | #' Adjust layer params and aesthetics (Layer operation) 2 | #' 3 | #' A layer [operation] for adjusting the params and aesthetic mappings of 4 | #' a [layer-like] object. 5 | #' 6 | #' @param object One of: 7 | #' - A [layer-like] object: applies this operation to the layer. 8 | #' - A missing argument: creates an [operation] 9 | #' - Anything else: creates an [operation], passing `object` along to the 10 | #' `mapping` argument 11 | #' @param mapping An aesthetic created using `aes()`. Mappings provided here 12 | #' will overwrite mappings in [ggplot2::layer()]s when this [operation] is applied to 13 | #' them. 14 | #' @param ... [ggplot2::layer()] parameters, such as would be passed to a `geom_...()` 15 | #' or `stat_...()` call. Params provided here will overwrite params in layers when 16 | #' this [operation] is applied to them. 17 | #' 18 | #' @template operation 19 | #' 20 | #' @examples 21 | #' 22 | #' library(ggplot2) 23 | #' 24 | #' # Here we use adjust() with nop() ( + 1) to create a copy of 25 | #' # the stat_smooth layer, putting a white outline around it. 26 | #' set.seed(1234) 27 | #' k = 1000 28 | #' data.frame( 29 | #' x = seq(1, 10, length.out = k), 30 | #' y = rnorm(k, seq(1, 2, length.out = k) + c(0, 0.5)), 31 | #' g = c("a", "b") 32 | #' ) |> 33 | #' ggplot(aes(x, y, color = g)) + 34 | #' geom_point() + 35 | #' stat_smooth(method = lm, formula = y ~ x, linewidth = 1.5, se = FALSE) * 36 | #' (adjust(aes(group = g), color = "white", linewidth = 4) + 1) + 37 | #' scale_color_brewer(palette = "Dark2") 38 | #' 39 | #' # (note this could also be done with copy_under()) 40 | #' 41 | #' @name adjust 42 | #' @aliases adjust-class 43 | #' @export 44 | setClass("adjust", representation(mapping = "ANY", params = "list"), contains = "operation") 45 | -------------------------------------------------------------------------------- /R/class-operation-affine-transform.R: -------------------------------------------------------------------------------- 1 | #' Translate, scale, and rotate ggplot2 layers (Layer operation) 2 | #' 3 | #' Transform objects within a single layer (geom) or across multiple layers (geoms) 4 | #' using affine transformations, like translation, scale, and rotation. Uses 5 | #' the built-in compositing support in graphical devices added in R 4.2. 6 | #' 7 | #' @param object One of: 8 | #' - A [layer-like] object: applies this operation to the layer. 9 | #' - A missing argument: creates an [operation] 10 | #' - A `numeric()` or `unit()` giving the x-axis translation, 11 | #' which takes the place of the `x` argument. 12 | #' @param x A `numeric()` or `unit()` giving the x translation to apply. 13 | #' @param y A `numeric()` or `unit()` giving the y translation to apply. 14 | #' @param width A `numeric()` or `unit()` giving the width. 15 | #' @param height A `numeric()` or `unit()` giving the height. 16 | #' @param angle A `numeric()` giving the angle to rotate, in degrees. 17 | #' @template operation 18 | #' 19 | #' @details 20 | #' Applies an affine transformation (translation, scaling, rotation) to a layer. 21 | #' 22 | #' **Note:** due to limitations in the implementation of scaling and rotation, 23 | #' currently these operations can only be performed relative to the center of 24 | #' the plot. In future versions, the translation and rotation origin may be 25 | #' configurable. 26 | #' 27 | #' @section Supported devices: 28 | #' Transformation is not currently supported by all graphics devices. As of this writing, 29 | #' at least `png(type = "cairo")`, `svg()`, and `cairo_pdf()` are known to support 30 | #' blending. 31 | #' 32 | #' `affine_transform()` attempts to auto-detect support for affine transformation using `dev.capabilities()`. 33 | #' You may receive a warning when using `affine_transform()` if it appears transformation is not 34 | #' supported by the current graphics device. This warning **either** means (1) 35 | #' your graphics device does not support transformation (in which case you should 36 | #' switch to one that does) or (2) your graphics device 37 | #' supports transformation but incorrectly reports that it does not. Unfortunately, 38 | #' not all graphics devices that support transformation appear to correctly *report* 39 | #' that they support transformation, so even if auto-detection fails, `blend()` will 40 | #' still attempt to apply the transformation, just in case. 41 | #' 42 | #' If the warning is issued and the output is still correctly transformed, this is 43 | #' likely a bug in the graphics device. You can report the bug to the authors of 44 | #' the graphics device if you wish; in the mean time, you can use 45 | #' `options(ggblend.check_affine_transform = FALSE)` to disable the check. 46 | #' 47 | #' @references 48 | #' Murrell, Paul (2021): 49 | #' [Groups, Compositing Operators, and Affine Transformations in R Graphics](https://www.stat.auckland.ac.nz/~paul/Reports/GraphicsEngine/groups/groups.html). 50 | #' The University of Auckland. Report. 51 | #' \doi{10.17608/k6.auckland.17009120.v1}. 52 | #' 53 | #' @examples 54 | #' \dontshow{old_options = options(ggblend.check_affine_transform = FALSE)} 55 | #' library(ggplot2) 56 | #' 57 | #' # a simple dataset: 58 | #' set.seed(1234) 59 | #' data.frame(x = rnorm(100), y = rnorm(100)) |> 60 | #' ggplot(aes(x, y)) + 61 | #' geom_point() + 62 | #' xlim(-5, 5) 63 | #' 64 | #' # we could scale and translate copies of the point cloud 65 | #' # (though I'm not sure why...) 66 | #' data.frame(x = rnorm(100), y = rnorm(100)) |> 67 | #' ggplot(aes(x, y)) + 68 | #' geom_point() * ( 69 | #' affine_transform(x = -unit(100, "pt"), width = 0.5) |> adjust(color = "red") + 70 | #' affine_transform(width = 0.5) + 71 | #' affine_transform(x = unit(100, "pt"), width = 0.5) |> adjust(color = "blue") 72 | #' ) + 73 | #' xlim(-5, 5) 74 | #' \dontshow{options(old_options)} 75 | #' @name affine_transform 76 | #' @aliases affine_transform-class 77 | #' @export 78 | setClass("affine_transform", slots = list(x = "ANY", y = "ANY", width = "ANY", height = "ANY", angle = "numeric"), contains = "operation") 79 | -------------------------------------------------------------------------------- /R/class-operation-blend.R: -------------------------------------------------------------------------------- 1 | #' Blend ggplot2 layers (Layer operation) 2 | #' 3 | #' Blend objects within a single layer (geom) or across multiple layers (geoms) 4 | #' using graphical blending modes, such as `"multiply"`, `"overlay"`, etc. Uses 5 | #' the built-in compositing support in graphical devices added in R 4.2. 6 | #' 7 | #' @param object One of: 8 | #' - A [layer-like] object: applies this operation to the layer. 9 | #' - A missing argument: creates an [operation] 10 | #' - A string (character vector of length 1) giving the name of a blend, 11 | #' which takes the place of the `blend` argument. 12 | #' @param blend The blend mode to use. The default mode, `"over"`, corresponds to 13 | #' the "usual" blend mode of drawing objects on top of each other. 14 | #' The list of supported blend modes depends on your graphical device 15 | #' (see Murrell 2021), and are listed in `dev.capabilities()$compositing`. 16 | #' Blend modes can include: `"clear"`, 17 | #' `"source"`, `"over"`, `"in"`, `"out"`, `"atop"`, `"dest"`, `"dest.over"`, 18 | #' `"dest.in"`, `"dest.out"`, `"dest.atop"`, `"xor"`, `"add"`, `"saturate"`, 19 | #' `"multiply"`, `"screen"`, `"overlay"`, `"darken"`, `"lighten"`, 20 | #' `"color.dodge"`, `"color.burn"`, `"hard.light"`, `"soft.light"`, 21 | #' `"difference"`, and `"exclusion"` 22 | #' 23 | #' Blend modes like `"multiply"`, `"darken"`, and `"lighten"` are particularly useful as they 24 | #' are *commutative*: the result is the same whichever order they are applied in. 25 | #' 26 | #' A warning is issued if the current graphics device does not appear to support 27 | #' the requested blend mode. In some cases this warning may be spurious, so 28 | #' it can be disabled by setting `options(ggblend.check_blend = FALSE)`. 29 | #' @param alpha A numeric between `0` and `1` (inclusive). The opacity of a 30 | #' transparency mask applied to objects prior to blending. 31 | #' @template operation 32 | #' 33 | #' @details 34 | #' If `object` is a single layer / geometry and the `partition` aesthetic *is not* set, every 35 | #' graphical object ([grob()]) output by the geometry will be blended together 36 | #' using the `blend` blend mode. If `alpha != 1`, a transparency mask with the 37 | #' provided alpha level will be applied to each grob before blending. 38 | #' 39 | #' If `object` is a single layer / geometry and the `partition` aesthetic *is* set, 40 | #' the geometry will be rendered for each subset of the data defined by the 41 | #' `partition` aesthetic, a transparency mask with the provided `alpha` level 42 | #' will be applied to each resulting group as a whole (if `alpha != 1`), then these groups 43 | #' will be blended together using the `blend` blend mode. 44 | #' 45 | #' If `object` is a list of layers / geometries, those layers will be rendered 46 | #' separately, a transparency mask with the provided `alpha` level 47 | #' will be applied to each layer as a whole (if `alpha != 1`), then these layers 48 | #' will be blended together using the `blend` blend mode. 49 | #' 50 | #' If a `blend()` is multiplied by a list of layers using `*`, it acts on each 51 | #' layer individually (as if each layer were passed to `blend()`). 52 | #' 53 | #' @section Supported devices: 54 | #' Blending is not currently supported by all graphics devices. As of this writing, 55 | #' at least `png(type = "cairo")`, `svg()`, and `cairo_pdf()` are known to support 56 | #' blending. 57 | #' 58 | #' `blend()` attempts to auto-detect support for blending using `dev.capabilities()`. 59 | #' You may receive a warning when using `blend()` if it appears blending is not 60 | #' supported by the current graphics device. This warning **either** means (1) 61 | #' your graphics device does not support blending (in which case you should 62 | #' switch to one that does) or (2) your graphics device 63 | #' supports blending but incorrectly reports that it does not. Unfortunately, 64 | #' not all graphics devices that support blending appear to correctly *report* 65 | #' that they support blending, so even if auto-detection fails, `blend()` will 66 | #' still attempt to apply the blend, just in case. 67 | #' 68 | #' If the warning is issued and the output is still correctly blended, this is 69 | #' likely a bug in the graphics device. You can report the bug to the authors of 70 | #' the graphics device if you wish; in the mean time, you can use 71 | #' `options(ggblend.check_blend = FALSE)` to disable the check. 72 | #' 73 | #' @references 74 | #' Murrell, Paul (2021): 75 | #' [Groups, Compositing Operators, and Affine Transformations in R Graphics](https://www.stat.auckland.ac.nz/~paul/Reports/GraphicsEngine/groups/groups.html). 76 | #' The University of Auckland. Report. 77 | #' \doi{10.17608/k6.auckland.17009120.v1}. 78 | #' 79 | #' @examples 80 | #' \dontshow{old_options = options(ggblend.check_blend = FALSE)} 81 | #' library(ggplot2) 82 | #' 83 | #' # create two versions of a dataset, where draw order can affect output 84 | #' set.seed(1234) 85 | #' df_a = data.frame(x = rnorm(500, 0), y = rnorm(500, 1), set = "a") 86 | #' df_b = data.frame(x = rnorm(500, 1), y = rnorm(500, 2), set = "b") 87 | #' df_ab = rbind(df_a, df_b) |> 88 | #' transform(order = "draw a then b") 89 | #' df_ba = rbind(df_b, df_a) |> 90 | #' transform(order = "draw b then a") 91 | #' df = rbind(df_ab, df_ba) 92 | #' 93 | #' # Using the "darken" blend mode, draw order does not matter: 94 | #' df |> 95 | #' ggplot(aes(x, y, color = set)) + 96 | #' geom_point(size = 3) |> blend("darken") + 97 | #' scale_color_brewer(palette = "Set2") + 98 | #' facet_grid(~ order) 99 | #' 100 | #' # Using the "multiply" blend mode, we can see density within groups: 101 | #' df |> 102 | #' ggplot(aes(x, y, color = set)) + 103 | #' geom_point(size = 3) |> blend("multiply") + 104 | #' scale_color_brewer(palette = "Set2") + 105 | #' facet_grid(~ order) 106 | #' 107 | #' # blend() on a single geom by default blends all grobs in that geom together 108 | #' # using the requested blend mode. If we wish to blend within specific data 109 | #' # subsets using normal blending ("over") but between subsets using the 110 | #' # requested blend mode, we can set the partition aesthetic. This will 111 | #' # make "multiply" behave more like "darken": 112 | #' df |> 113 | #' ggplot(aes(x, y, color = set, partition = set)) + 114 | #' geom_point(size = 3) |> blend("multiply") + 115 | #' scale_color_brewer(palette = "Set2") + 116 | #' facet_grid(~ order) 117 | #' 118 | #' # We can also blend lists of geoms together; these geoms are rendered using 119 | #' # normal ("over") blending (unless a blend() call is applied to a specific 120 | #' # sub-layer, as in the first layer below) and then blended together using 121 | #' # the requested blend mode. 122 | #' df |> 123 | #' ggplot(aes(x, y, color = set)) + 124 | #' list( 125 | #' geom_point(size = 3) |> blend("darken"), 126 | #' geom_vline(xintercept = 0, color = "gray75", linewidth = 1.5), 127 | #' geom_hline(yintercept = 0, color = "gray75", linewidth = 1.5) 128 | #' ) |> blend("hard.light") + 129 | #' scale_color_brewer(palette = "Set2") + 130 | #' facet_grid(~ order) 131 | #' \dontshow{options(old_options)} 132 | #' @name blend 133 | #' @aliases blend-class 134 | #' @export 135 | setClass("blend", representation(blend = "character", alpha = "numeric"), contains = "operation") 136 | -------------------------------------------------------------------------------- /R/class-operation-composition.R: -------------------------------------------------------------------------------- 1 | #' Layer operation composition 2 | #' 3 | #' [operation]s can be composed together to form chains of operations, which 4 | #' when multiplied by (applied to) [layer-like] objects, return modified [layer-like] objects. In 5 | #' contrast to [operation_product]s, compositions of operations are not 6 | #' distributive over sums of [operation]s or [layer-like] objects. 7 | #' 8 | #' @details 9 | #' 10 | #' Operation composition is achieved through function application, typically 11 | #' using the pipe operator (`|>`); e.g. `operation1 |> operation2`. 12 | #' 13 | #' The output of composing \pkg{ggblend} [operation]s depends on the types of 14 | #' objects being composed: 15 | #' 16 | #' - If you compose an [operation] with an [operation], they are merged into 17 | #' a single [operation] that applies each [operation] in sequence, without 18 | #' distributing over layers. 19 | #' - If you compose an [operation] with a [layer-like] object, that operation is applied 20 | #' to the layer, returning a new [layer-like] object. The operation is applied to the 21 | #' layer as a whole, not any sub-parts (e.g. sub-layers or graphical objects). 22 | #' 23 | #' @returns An [operation]. 24 | #' 25 | #' @examples 26 | #' \dontshow{old_options = options(ggblend.check_blend = FALSE)} 27 | #' library(ggplot2) 28 | #' 29 | #' # composing operations together chains them 30 | #' adjust(color = "red") |> blend("multiply") 31 | #' 32 | #' # unlike multiplication, composition does not follow the distributive law 33 | #' mult_op = (adjust(aes(y = 11 -x), color = "skyblue") + 1) * blend("multiply") 34 | #' mult_op 35 | #' 36 | #' comp_op = (adjust(aes(y = 11 -x), color = "skyblue") + 1) |> blend("multiply") 37 | #' comp_op 38 | #' 39 | #' # multiplication by a geom returns a modified version of that geom 40 | #' data.frame(x = 1:10) |> 41 | #' ggplot(aes(x = x, y = x)) + 42 | #' geom_line(linewidth = 10, color = "red") * comp_op 43 | #' \dontshow{options(old_options)} 44 | #' @name operation_composition 45 | #' @aliases operation_composition-class 46 | #' @export 47 | setClass("operation_composition", representation(operation1 = "operation", operation2 = "operation"), contains = c("operation")) 48 | -------------------------------------------------------------------------------- /R/class-operation-nop.R: -------------------------------------------------------------------------------- 1 | #' Identity ("no-op") transformation (Layer operation) 2 | #' 3 | #' A layer [operation] which returns the input [layer-like] object unchanged. 4 | #' 5 | #' @param object One of: 6 | #' - A [layer-like] object: applies this operation to the layer. 7 | #' - A missing argument: creates an [operation] 8 | #' 9 | #' @details 10 | #' 11 | #' When `numeric()`s are used with [operation]s, they are converted into 12 | #' sums of `nop()`s. 13 | #' 14 | #' @template operation 15 | #' 16 | #' @examples 17 | #' library(ggplot2) 18 | #' 19 | #' # adding a nop to another operation is equivalent to adding a numeric 20 | #' adjust() + nop() 21 | #' 22 | #' # and vice versa 23 | #' adjust() + 2 24 | #' 25 | #' # here we use adjust() with nop() ( + 1) to create a copy of 26 | #' # the stat_smooth layer, putting a white outline around it. 27 | #' set.seed(1234) 28 | #' k = 1000 29 | #' data.frame( 30 | #' x = seq(1, 10, length.out = k), 31 | #' y = rnorm(k, seq(1, 2, length.out = k) + c(0, 0.5)), 32 | #' g = c("a", "b") 33 | #' ) |> 34 | #' ggplot(aes(x, y, color = g)) + 35 | #' geom_point() + 36 | #' stat_smooth(method = lm, formula = y ~ x, linewidth = 1.5, se = FALSE) * 37 | #' (adjust(aes(group = g), color = "white", linewidth = 4) + 1) + 38 | #' scale_color_brewer(palette = "Dark2") 39 | #' 40 | #' # (note this could also be done with copy_under()) 41 | #' 42 | #' @name nop 43 | #' @aliases nop-class 44 | #' @export 45 | setClass("nop", contains = "operation") 46 | -------------------------------------------------------------------------------- /R/class-operation-product.R: -------------------------------------------------------------------------------- 1 | #' Layer operation products 2 | #' 3 | #' [operation]s can be multiplied together to form chains of operations, which 4 | #' when multiplied by (applied to) [layer-like] objects, return modified [layer-like] objects. 5 | #' 6 | #' @param x,... [operation]s 7 | #' @param e1 an [operation], [layer-like], or [numeric()] 8 | #' @param e2 an [operation], [layer-like], or [numeric()] 9 | #' @param na.rm ignored 10 | #' 11 | #' @details 12 | #' Multiplication of \pkg{ggblend} [operation]s depends on the types of 13 | #' objects being multiplied: 14 | #' 15 | #' - If you multiply an [operation] with an [operation], they are merged into 16 | #' a single [operation] that applies each [operation] in sequence. 17 | #' - If you multiply an [operation] with a [layer-like] object, that operation is applied 18 | #' to the layer, returning a new [layer-like] object. 19 | #' - If you multiply an [operation] by a [numeric()] *n*, a new [operation] that 20 | #' repeats the input [operation] is *n* times is returned. 21 | #' 22 | #' @returns An [operation]. 23 | #' 24 | #' @examples 25 | #' library(ggplot2) 26 | #' 27 | #' # multiplying operations by numerics repeats them... 28 | #' adjust(color = "red") * 2 29 | #' 30 | #' # multiplying operations together chains (or merges) them 31 | #' adjust(color = "red") * adjust(linewidth = 2) 32 | #' 33 | #' # multiplication obeys the distributive law 34 | #' op = (adjust(aes(y = 11 -x), color = "skyblue") + 1) * (adjust(color = "white", linewidth = 4) + 1) 35 | #' op 36 | #' 37 | #' # multiplication by a geom returns a modified version of that geom 38 | #' data.frame(x = 1:10) |> 39 | #' ggplot(aes(x = x, y = x)) + 40 | #' geom_line(linewidth = 2) * op 41 | #' 42 | #' @name operation_product 43 | #' @aliases operation_product-class 44 | #' @export 45 | setClass("operation_product", contains = c("list", "operation")) 46 | -------------------------------------------------------------------------------- /R/class-operation-sum.R: -------------------------------------------------------------------------------- 1 | #' Layer operation sums 2 | #' 3 | #' [operation]s can be added together to form stacks of operations, which 4 | #' when multiplied by (applied to) [layer-like] objects, those [layer-like] objects are distributed 5 | #' over the [operation]s (i.e. copied). 6 | #' 7 | #' @param x,... [operation]s 8 | #' @param e1 an [operation] or [numeric()] 9 | #' @param e2 an [operation] or [numeric()] 10 | #' @param na.rm ignored 11 | #' 12 | #' @details 13 | #' Addition of \pkg{ggblend} [operation]s depends on the types of 14 | #' objects being summed: 15 | #' 16 | #' - If you add an [operation] to an [operation], they are merged into 17 | #' a single [operation] that copies input [layer-like] objects, one for each [operation]. 18 | #' - If you add an [operation] to a [numeric()] *n*, it is equivalent to 19 | #' adding `*` [nop()]s to that [operation]. 20 | #' 21 | #' @returns An [operation]. 22 | #' 23 | #' @examples 24 | #' library(ggplot2) 25 | #' 26 | #' # adding operations together creates a sum of operations 27 | #' adjust(color = "red") + adjust(linewidth = 2) 28 | #' 29 | #' # addition and multiplication obey the distributive law 30 | #' op = (adjust(aes(y = 11 -x), color = "skyblue") + 1) * (adjust(color = "white", linewidth = 4) + 1) 31 | #' op 32 | #' 33 | #' # multiplication by a geom returns a modified version of that geom, 34 | #' # distributed over the sum of the operations 35 | #' data.frame(x = 1:10) |> 36 | #' ggplot(aes(x = x, y = x)) + 37 | #' geom_line(linewidth = 2) * op 38 | #' @name operation_sum 39 | #' @aliases operation_sum-class 40 | #' @export 41 | setClass("operation_sum", contains = c("list", "operation")) 42 | -------------------------------------------------------------------------------- /R/ggblend-package.R: -------------------------------------------------------------------------------- 1 | #' Blending and compositing for ggplot2 2 | #' 3 | #' @docType package 4 | #' @name ggblend-package 5 | #' @aliases ggblend 6 | #' 7 | #' @description 8 | #' 9 | #' \pkg{ggblend} is an R package that adds support for R 4.2 blend modes 10 | #' (e.g. `"multiply"`, `"overlay"`, etc) to \pkg{ggplot2}. 11 | #' 12 | #' @details 13 | #' 14 | #' The primary support for blending is provided by the `blend()` function, 15 | #' which can be used to augment [ggplot()] layers/geoms or lists of 16 | #' layers/geoms in a [ggplot()] specification. 17 | #' 18 | #' For example, one can replace something like this: 19 | #' 20 | #' ``` 21 | #' df |> 22 | #' ggplot(aes(x, y)) + 23 | #' geom_X(...) + 24 | #' geom_Y(...) + 25 | #' geom_Z(...) 26 | #' ``` 27 | #' 28 | #' With something like this: 29 | #' 30 | #' ``` 31 | #' df |> 32 | #' ggplot(aes(x, y)) + 33 | #' geom_X(...) + 34 | #' geom_Y(...) |> blend("multiply") + 35 | #' geom_Z(...) 36 | #' ``` 37 | #' 38 | #' In order to apply a "multiply" blend to the layer with `geom_Y(...)`. 39 | #' 40 | #' @section Package options: 41 | #' 42 | #' The following global options can be set using [options()] to modify the 43 | #' behavior of \pkg{ggblend}: 44 | #' 45 | #' - `"ggblend.check_blend"`: If `TRUE` (default), [blend()] will warn if 46 | #' you attempt to use a blend mode not supported by the current graphics 47 | #' device, as reported by `dev.capabilities()$compositing`. Since this check 48 | #' can be unreliable on some devices (they will report not support a blend 49 | #' mode that they do support), you can disable this warning by setting this 50 | #' option to `FALSE`. 51 | #' 52 | #' - `"ggblend.check_affine_transform"`: If `TRUE` (default), [affine_transform()] will warn if 53 | #' you attempt to use a blend mode not supported by the current graphics 54 | #' device, as reported by `dev.capabilities()$transformation`. Since this check 55 | #' can be unreliable on some devices (they will report not support a blend 56 | #' mode that they do support), you can disable this warning by setting this 57 | #' option to `FALSE`. 58 | #' 59 | #' @import grid 60 | #' @import ggplot2 61 | #' @import methods 62 | NULL 63 | -------------------------------------------------------------------------------- /R/layer-.R: -------------------------------------------------------------------------------- 1 | #' ggplot2 layer-like objects 2 | #' 3 | #' For technical reasons related to how \pkg{ggplot2} implements layers, there 4 | #' is no single class from which all valid \pkg{ggplot2} layers and lists of 5 | #' layers inherit. Thus, \pkg{ggblend} [operation]s supports a variety of "layer-like" 6 | #' objects, documented here (see *Details*). 7 | #' 8 | #' @param x A [layer-like] object. See *Details*. 9 | #' 10 | #' @details 11 | #' \pkg{ggblend} [operation]s can be applied to several [ggplot2::layer()]-like objects, 12 | #' including: 13 | #' 14 | #' - objects of class `"LayerInstance"`; e.g. `stat`s and `geom`s. 15 | #' - [list()]s of layer-like objects. 16 | #' - [layer_list()]s, which are a more type-safe version of [list()]s of 17 | #' layer-like objects. 18 | #' 19 | #' Anywhere in \pkg{ggblend} where a function parameter is documented as being 20 | #' [layer-like], it can be any of the above object types. 21 | #' 22 | #' @examples 23 | #' library(ggplot2) 24 | #' 25 | #' is_layer_like(geom_line()) 26 | #' is_layer_like(list(geom_line())) 27 | #' is_layer_like(list(geom_line(), scale_x_continuous())) 28 | #' is_layer_like(list(geom_line(), "abc")) 29 | #' @name layer-like 30 | #' @aliases layer 31 | NULL 32 | 33 | 34 | # type predicates --------------------------------------------------------- 35 | 36 | #' @describeIn layer-like checks if an object is layer-like according to \pkg{ggblend}. 37 | #' @returns For `is_layer_like()`, a `logical`: `TRUE` if `x` is layer-like, `FALSE` otherwise. 38 | #' @export 39 | is_layer_like = function(x) { 40 | inherits(x, c("LayerInstance", "layer_list")) || .is_layer_list_like(x) 41 | } 42 | 43 | .is_layer_list_like = function(x) { 44 | is.list(x) && all(vapply(x, .is_layer_list_element, logical(1))) 45 | } 46 | 47 | .is_layer_list_element = function(x) { 48 | inherits(x, c("new_aes", "gg")) || .is_layer_list_like(x) 49 | } 50 | 51 | # type conversion --------------------------------------------------------- 52 | 53 | #' @describeIn layer-like validates that an object is layer-like and converts 54 | #' it to a `"LayerInstance"` or [layer_list()]. 55 | #' @returns For `as_layer_like()`, a `"LayerInstance"` or a [layer_list()]. 56 | #' @export 57 | as_layer_like = function(x) { 58 | UseMethod("as_layer_like") 59 | } 60 | 61 | #' @rdname layer-like 62 | #' @export 63 | as_layer_like.default = function(x) { 64 | stop0("Cannot convert object of type ", deparse1(class(x)), " to a layer-like object") 65 | } 66 | 67 | #' @rdname layer-like 68 | #' @export 69 | as_layer_like.LayerInstance = function(x) { 70 | x 71 | } 72 | 73 | #' @rdname layer-like 74 | #' @export 75 | as_layer_like.list = function(x) { 76 | as_layer_list(x) 77 | } 78 | 79 | #' @rdname layer-like 80 | #' @export 81 | as_layer_like.layer_list = function(x) x 82 | 83 | 84 | # layer manipulation ------------------------------------------------------ 85 | 86 | #' Apply a function over a layer, returning an object of the same type of layer 87 | #' (pure layer, layer list, or layer group) 88 | #' @noRd 89 | layer_apply = function(.x, .f, ...) { 90 | UseMethod("layer_apply") 91 | } 92 | 93 | #' @export 94 | layer_apply.LayerInstance = function(.x, .f, ...) { 95 | .f(.x, ...) 96 | } 97 | 98 | #' @export 99 | layer_apply.list = function(.x, .f, ...) { 100 | rapply(.x, .f, classes = "LayerInstance", how = "replace", ...) 101 | } 102 | -------------------------------------------------------------------------------- /R/layer-list.R: -------------------------------------------------------------------------------- 1 | new_layer_list = function(list) { 2 | new("layer_list", .Data = list) 3 | } 4 | 5 | #' @rdname layer_list 6 | #' @export 7 | layer_list = function(...) { 8 | new_layer_list(list(...)) 9 | } 10 | 11 | 12 | # type conversion --------------------------------------------------------- 13 | 14 | #' @rdname layer_list 15 | #' @export 16 | as_layer_list = function(x) { 17 | UseMethod("as_layer_list") 18 | } 19 | 20 | #' @rdname layer_list 21 | #' @export 22 | as_layer_list.layer_list = function(x) { 23 | x 24 | } 25 | 26 | #' @rdname layer_list 27 | #' @export 28 | as_layer_list.list = function(x) { 29 | if (!is_layer_like(x)) { 30 | stop0("All objects in a layer_list must be layer-like objects") 31 | } 32 | new_layer_list(as.list(x)) 33 | } 34 | 35 | #' @rdname layer_list 36 | #' @export 37 | as_layer_list.LayerInstance = function(x) { 38 | new_layer_list(list(x)) 39 | } 40 | 41 | 42 | # layer concatenation ------------------------------------------------- 43 | 44 | #' @rdname layer_list 45 | #' @export 46 | setMethod("+", signature(e1 = "layer_list", e2 = "layer_list"), function(e1, e2) { 47 | new_layer_list(c(e1, e2)) 48 | }) 49 | 50 | 51 | # layer list flattening --------------------------------------------------- 52 | 53 | #' Flatten a list of layers so that nested lists of layers (a la list(..., ..., list(...))) 54 | #' are flattened into a single level. Unlike unlist(), does not remove type information 55 | #' of list elements / try to coerce everything to the same vector type 56 | #' @noRd 57 | flatten_layer_list = function(layers) { 58 | if (is.list(layers)) { 59 | do.call(c, lapply(layers, flatten_layer_list)) 60 | } else { 61 | list(layers) 62 | } 63 | } 64 | 65 | 66 | # printing ---------------------------------------------------------------- 67 | 68 | #' @rdname layer_list 69 | #' @export 70 | setMethod("show", signature(object = "layer_list"), function(object) { 71 | cat(":\n") 72 | print(object@.Data) 73 | }) 74 | -------------------------------------------------------------------------------- /R/operation-.R: -------------------------------------------------------------------------------- 1 | 2 | # construct an operation -------------------------------------------------- 3 | 4 | #' Make a function that can make an operation 5 | #' @param name string: name of the operation 6 | #' @param constructor: bare (unevaluated) name of constructor function 7 | #' @param y bare (unevaluated) name of an argument to `constructor` that 8 | #' will be filled in with the first argument to the operation when the 9 | #' operation is not immediately applied to a layer. Optional. 10 | #' @noRd 11 | make_operation = function(name, constructor, y) { 12 | # construct (x = x, y = y, ... = ..., etc) arg 13 | # list for calling the constructor 14 | args = formals(constructor) 15 | constructor_args = lapply(names(args), as.symbol) 16 | names(constructor_args) = names(args) 17 | .constructor = substitute(constructor) 18 | 19 | # construct args for the output function 20 | f_args = c(alist(object = ), args) 21 | 22 | # if y is provided, it is an argument that object will be copied into if 23 | # we are constructing the operator (and not applying it directly) 24 | copy_object_to_y = if (!missing(y)) { 25 | y = substitute(y) 26 | y_string = as.character(y) 27 | 28 | bquote( 29 | if (not_missing_object) { 30 | if (!missing(.(y))) { 31 | stop0( 32 | "Cannot provide both the `object` and `", .(y_string), 33 | "` arguments to `", .(name), "()` simultaneously." 34 | ) 35 | } 36 | .(y) = object 37 | } 38 | ) 39 | } 40 | 41 | f_body = bquote(splice = TRUE, { 42 | not_missing_object = !missing(object) 43 | if (not_missing_object && is_layer_like(object)) { 44 | operation = .(.constructor)(..(constructor_args)) 45 | layer = as_layer_like(object) 46 | apply_composed_operation(operation, layer) 47 | } else if (not_missing_object && is(object, "operation")) { 48 | operation = .(.constructor)(..(constructor_args)) 49 | new_operation_composition(object, operation) 50 | } else { 51 | .(copy_object_to_y) 52 | .(.constructor)(..(constructor_args)) 53 | } 54 | }) 55 | 56 | as.function(c(f_args, f_body), envir = parent.frame()) 57 | } 58 | 59 | 60 | # type conversion --------------------------------------------------------- 61 | 62 | setAs("numeric", "operation", function(from) { 63 | from * nop() 64 | }) 65 | 66 | setAs("list", "operation", function(from) { 67 | as(from, "operation_sum") 68 | }) 69 | 70 | 71 | # operation application --------------------------------------------------- 72 | 73 | setGeneric("apply_operation", function(operation, layers) { 74 | stop0("Unimplemented layer operation") 75 | }) 76 | 77 | setGeneric("apply_composed_operation", function(operation, layers) { 78 | apply_operation(operation, layers) 79 | }) 80 | 81 | #' @rdname operation_product 82 | #' @export 83 | setMethod("*", signature(e1 = "operation"), function(e1, e2) { 84 | apply_operation(e1, as_layer_like(e2)) 85 | }) 86 | 87 | #' @rdname operation_product 88 | #' @export 89 | setMethod("*", signature(e2 = "operation"), function(e1, e2) { 90 | apply_operation(e2, as_layer_like(e1)) 91 | }) 92 | 93 | 94 | # printing ---------------------------------------------------------------- 95 | 96 | #' @describeIn operation Print an [operation]. 97 | #' @returns For `show()`, an [invisible()] copy of the input. 98 | #' @export 99 | setMethod("show", signature(object = "operation"), function(object) { 100 | cat0(": ", format(object), "\n") 101 | invisible(object) 102 | }) 103 | 104 | #' @describeIn operation Format an [operation] for printing. 105 | #' @returns For `format()`, a character string representing the input. 106 | #' @export 107 | setMethod("format", signature(x = "operation"), function(x, ...) { 108 | arg_names = setdiff(slotNames(x), ".Data") 109 | args = attributes(x)[arg_names] 110 | args_string = format_name_value_pairs(args) 111 | paste0(tolower(class(x)), "(", args_string, ")") 112 | }) 113 | 114 | -------------------------------------------------------------------------------- /R/operation-adjust.R: -------------------------------------------------------------------------------- 1 | new_adjust = function(mapping = aes(), ...) { 2 | names(mapping) = standardise_aes_names(names(mapping)) 3 | 4 | params = list(...) 5 | names(params) = standardise_aes_names(names(params)) 6 | 7 | new("adjust", mapping = mapping, params = params) 8 | } 9 | 10 | #' @rdname adjust 11 | #' @export 12 | adjust = make_operation("adjust", new_adjust, mapping) 13 | 14 | 15 | # operation application --------------------------------------------------- 16 | 17 | setMethod("apply_operation", signature(operation = "adjust"), function(operation, layers) { 18 | params = operation@params 19 | mapping = operation@mapping 20 | 21 | layer_apply(layers, function(layer) { 22 | l = ggproto(NULL, layer) 23 | 24 | l$mapping[names(mapping)] = mapping 25 | if (!is.null(l$mapping)) { 26 | class(l$mapping) = "uneval" 27 | } 28 | 29 | aes_param_names = intersect(names(params), l$geom$aesthetics()) 30 | l$aes_params[aes_param_names] = params[aes_param_names] 31 | geom_param_names = intersect(names(params), l$geom$parameters(TRUE)) 32 | l$geom_params[geom_param_names] = params[geom_param_names] 33 | stat_param_names = intersect(names(params), l$stat$parameters(TRUE)) 34 | l$stat_params[stat_param_names] = params[stat_param_names] 35 | 36 | l 37 | }) 38 | }) 39 | 40 | 41 | # operation multiplication ------------------------------------------------- 42 | 43 | #' @rdname operation_product 44 | #' @export 45 | setMethod("*", signature(e1 = "adjust", e2 = "adjust"), function(e1, e2) { 46 | e1@mapping[names(e2@mapping)] = e2@mapping 47 | e1@params[names(e2@params)] = e2@params 48 | e1 49 | }) 50 | 51 | 52 | # printing ---------------------------------------------------------------- 53 | 54 | #' @rdname operation-class 55 | #' @export 56 | setMethod("format", signature(x = "adjust"), function(x, ...) { 57 | mapping_string = paste0("aes(", format_name_value_pairs(x@mapping), ")", recycle0 = TRUE) 58 | params_string = format_name_value_pairs(x@params) 59 | args_string = paste0(c(mapping_string, params_string), collapse = ", ", recycle0 = TRUE) 60 | paste0(tolower(class(x)), "(", args_string, ")") 61 | }) 62 | -------------------------------------------------------------------------------- /R/operation-affine-transform.R: -------------------------------------------------------------------------------- 1 | new_affine_transform = function(x = 0, y = 0, width = 1, height = 1, angle = 0) { 2 | x = check_unit(x) 3 | y = check_unit(y) 4 | width = check_unit(width) 5 | height = check_unit(height) 6 | 7 | new("affine_transform", x = x, y = y, width = width, height = height, angle = angle) 8 | } 9 | 10 | #' @rdname affine_transform 11 | #' @export 12 | affine_transform = make_operation("affine_transform", new_affine_transform, x) 13 | 14 | 15 | # operation application --------------------------------------------------- 16 | 17 | setMethod("apply_operation", signature(operation = "affine_transform"), function(operation, layers) { 18 | grob_transform = function(grob) affine_transform_grob( 19 | grob, 20 | x = operation@x, y = operation@y, 21 | width = operation@width, height = operation@height, 22 | angle = operation@angle 23 | ) 24 | layer_apply(layers, transform_layer, grob_transform = grob_transform, check = check_affine_transform) 25 | }) 26 | 27 | setMethod("apply_composed_operation", signature(operation = "affine_transform", layers = "list"), function(operation, layers) { 28 | grob_transform = function(grob) affine_transform_grob( 29 | grob, 30 | x = operation@x, y = operation@y, 31 | width = operation@width, height = operation@height, 32 | angle = operation@angle 33 | ) 34 | transform_layers(layers, grob_transform, check = check_affine_transform) 35 | }) 36 | 37 | 38 | # printing ---------------------------------------------------------------- 39 | 40 | #' @rdname operation-class 41 | #' @export 42 | setMethod("format", signature(x = "affine_transform"), function(x, ...) { 43 | defaults = list(x = unit(0, "npc"), y = unit(0, "npc"), width = unit(1, "npc"), height = unit(1, "npc"), angle = 0) 44 | args_string = format_name_value_pairs(attributes(x)[names(defaults)], defaults) 45 | paste0(tolower(class(x)), "(", args_string, ")") 46 | }) 47 | 48 | 49 | # affine transform for grobs ----------------------------------------------------- 50 | 51 | #' Apply affine transform to a grob 52 | #' @param x x translation (a unit) 53 | #' @param y y translation (a unit) 54 | #' @param width width (a unit) 55 | #' @param height height (a unit) 56 | #' @param angle angle of rotation (a numeric) 57 | #' @return a grob 58 | #' @noRd 59 | affine_transform_grob = function(grob, x, y, width, height, angle) { 60 | dg = defineGrob(grob) 61 | 62 | grobTree(dg, 63 | grobTree( 64 | useGrob(dg$name), 65 | vp = viewport( 66 | x = unit(0.5, "npc") + x, y = unit(0.5, "npc") + y, 67 | width = width, height = height, angle = angle 68 | ) 69 | ) 70 | ) 71 | } 72 | 73 | #' check that an argument is a `unit()` 74 | #' @param arg argument that should be a `unit()` 75 | #' @noRd 76 | check_unit = function(arg) { 77 | if (is.unit(arg)) { 78 | arg 79 | } else if (is.numeric(arg)) { 80 | unit(arg, "npc") 81 | } else { 82 | stop0( 83 | deparse1(substitute(arg)), " argument to affine_transform() has class ", 84 | deparse1(class(arg)), ". It must be a numeric or a grid::unit()." 85 | ) 86 | } 87 | } 88 | 89 | #' Check to see if transformations are supported by the current 90 | #' graphics device, issuing a warning if not 91 | #' @noRd 92 | check_affine_transform = function() { 93 | if ( 94 | getOption("ggblend.check_affine_transform", TRUE) && 95 | grDevices::dev.cur() != 1 && 96 | !isTRUE(grDevices::dev.capabilities()$transformations) 97 | ) { 98 | warning0( 99 | 'Your graphics device, ', deparse1(names(grDevices::dev.cur())), 100 | ', reports that affine transformations are not supported.\n', 101 | bullet('If the transformed output IS NOT as expected (e.g. geoms are not being 102 | drawn), then you must switch to a graphics device that supports transformations, 103 | like png(type = "cairo"), svg(), or cairo_pdf(). 104 | '), "\n", 105 | bullet('If the transformed output IS as expected despite this warning, this is likely a 106 | bug *in the graphics device*. Unfortunately, several graphics do not correctly 107 | report their capabilities. You may wish to a report a bug to the authors of the 108 | graphics device. In the mean time, you can disable this 109 | warning via options(ggblend.check_affine_transform = FALSE). 110 | '), "\n", 111 | bullet("For more information, see the Supported Devices section of help('affine_transform').") 112 | ) 113 | } 114 | invisible(NULL) 115 | } 116 | -------------------------------------------------------------------------------- /R/operation-blend.R: -------------------------------------------------------------------------------- 1 | new_blend = function(blend = "over", alpha = 1) { 2 | new("blend", blend = blend, alpha = alpha) 3 | } 4 | 5 | #' @rdname blend 6 | #' @export 7 | blend = make_operation("blend", new_blend, blend) 8 | 9 | 10 | # operation application --------------------------------------------------- 11 | 12 | setMethod("apply_operation", signature(operation = "blend"), function(operation, layers) { 13 | grob_transform = function(grob) blend_grob(grob, blend = operation@blend, alpha = operation@alpha) 14 | check = function() check_blend(operation@blend) 15 | layer_apply(layers, transform_layer, grob_transform = grob_transform, check = check) 16 | }) 17 | 18 | setMethod("apply_composed_operation", signature(operation = "blend", layers = "list"), function(operation, layers) { 19 | # this is kind of hacky but seems to work --- basically, make a list of 20 | # layers where all the input layers are hidden layers (when $draw_geom() is 21 | # called on them it saves the data needed for drawing but otherwise does 22 | # nothing), and the final layer actually draws each layer and then blends 23 | # them together 24 | grob_transform = function(grob) blend_grob(grob, blend = operation@blend, alpha = operation@alpha) 25 | check = function() check_blend(operation@blend) 26 | transform_layers(layers, grob_transform, check = check) 27 | }) 28 | 29 | 30 | # printing ---------------------------------------------------------------- 31 | 32 | #' @rdname operation-class 33 | #' @export 34 | setMethod("format", signature(x = "blend"), function(x, ...) { 35 | blend_string = if (x@blend != "over") deparse1(x@blend) 36 | alpha_string = if (x@alpha != 1) paste0("alpha = ", deparse1(x@alpha)) 37 | args_string = paste0(c(blend_string, alpha_string), collapse = ", ", recycle0 = TRUE) 38 | paste0(tolower(class(x)), "(", args_string, ")") 39 | }) 40 | 41 | 42 | # blending grobs ---------------------------------------------------------- 43 | 44 | #' Blend a grob 45 | #' @param grob grob to blend 46 | #' @param blend a blend mode 47 | #' @param alpha alpha of a transparency mask to be applied to each grob before the blend 48 | #' @return a grob 49 | #' @noRd 50 | blend_grob = function(grob, blend = "over", alpha = 1) { 51 | viewport = if (!isTRUE(alpha == 1)) { 52 | mask = rectGrob(gp = gpar(col = NA, fill = grDevices::rgb(0, 0, 0, alpha))) 53 | viewport(mask = mask) 54 | } 55 | groupGrob(grob, blend, vp = viewport) 56 | } 57 | 58 | #' Check to see if blending (compositing) is supported by the current 59 | #' graphics device, issuing a warning if it is not 60 | #' @param blend Blend mode to check for. One of: 61 | #' - `NULL`: disable blending 62 | #' - A string representing a compositing operator that can be passed to the 63 | #' `op` argument of `grid::groupGrob()` 64 | #' @return `blend` (invisibly) 65 | #' @noRd 66 | check_blend = function(blend) { 67 | if ( 68 | getOption("ggblend.check_blend", TRUE) && 69 | grDevices::dev.cur() != 1 && 70 | !isTRUE(blend %in% grDevices::dev.capabilities()$compositing) 71 | ) { 72 | warning0( 73 | 'Your graphics device, ', deparse1(names(grDevices::dev.cur())), 74 | ', reports that blend = ', deparse1(blend), ' is not supported.\n', 75 | bullet('If the blending output IS NOT as expected (e.g. geoms are not being 76 | drawn), then you must switch to a graphics device that supports blending, 77 | like png(type = "cairo"), svg(), or cairo_pdf(). 78 | '), "\n", 79 | bullet('If the blending output IS as expected despite this warning, this is likely a 80 | bug *in the graphics device*. Unfortunately, several graphics do not correctly 81 | report their capabilities. You may wish to a report a bug to the authors of the 82 | graphics device. In the mean time, you can disable this 83 | warning via options(ggblend.check_blend = FALSE). 84 | '), "\n", 85 | bullet("For more information, see the Supported Devices section of help('blend').") 86 | ) 87 | } 88 | invisible(blend) 89 | } 90 | -------------------------------------------------------------------------------- /R/operation-composition.R: -------------------------------------------------------------------------------- 1 | new_operation_composition = function(operation1, operation2) { 2 | new("operation_composition", operation1 = operation1, operation2 = operation2) 3 | } 4 | 5 | 6 | # operation application --------------------------------------------------- 7 | 8 | setMethod("apply_operation", signature(operation = "operation_composition"), function(operation, layers) { 9 | apply_composed_operation(operation@operation2, layers * operation@operation1) 10 | }) 11 | 12 | 13 | # printing ---------------------------------------------------------------- 14 | 15 | #' @rdname operation-class 16 | #' @export 17 | setMethod("format", signature(x = "operation_composition"), function(x, ...) { 18 | format1 = format(x@operation1, ...) 19 | format2 = format(x@operation2, ...) 20 | paste0(format1, " |> ", format2) 21 | }) 22 | -------------------------------------------------------------------------------- /R/operation-copy.R: -------------------------------------------------------------------------------- 1 | #' Copy layers then adjust params and aesthetics (Layer operation) 2 | #' 3 | #' A layer [operation] for copying and then adjusting the params and aesthetic 4 | #' mappings of a [layer-like] object. 5 | #' 6 | #' @inheritParams adjust 7 | #' @template operation 8 | #' 9 | #' @details 10 | #' These are shortcuts for duplicating a layer and then applying [adjust()]. 11 | #' Specifically: 12 | #' 13 | #' - `copy_over(...)` is equivalent to `1 + adjust(...)` 14 | #' - `copy_under(...)` is equivalent to `adjust(...) + 1` 15 | #' 16 | #' @examples 17 | #' library(ggplot2) 18 | #' 19 | #' # here we use copy_under() to create a copy of 20 | #' # the stat_smooth layer, putting a white outline around it. 21 | #' set.seed(1234) 22 | #' k = 1000 23 | #' data.frame( 24 | #' x = seq(1, 10, length.out = k), 25 | #' y = rnorm(k, seq(1, 2, length.out = k) + c(0, 0.5)), 26 | #' g = c("a", "b") 27 | #' ) |> 28 | #' ggplot(aes(x, y, color = g)) + 29 | #' geom_point() + 30 | #' stat_smooth(method = lm, formula = y ~ x, linewidth = 1.5, se = FALSE) * 31 | #' copy_under(aes(group = g), color = "white", linewidth = 4) + 32 | #' scale_color_brewer(palette = "Dark2") 33 | #' 34 | #' @name copy 35 | NULL 36 | 37 | new_copy_over = function(mapping = aes(), ...) { 38 | 1 + adjust(mapping = mapping, ...) 39 | } 40 | 41 | #' @rdname copy 42 | #' @export 43 | copy_over = make_operation("copy_over", new_copy_over, mapping) 44 | 45 | new_copy_under = function(mapping = aes(), ...) { 46 | adjust(mapping = mapping, ...) + 1 47 | } 48 | 49 | #' @rdname copy 50 | #' @export 51 | copy_under = make_operation("copy_over", new_copy_under, mapping) 52 | -------------------------------------------------------------------------------- /R/operation-nop.R: -------------------------------------------------------------------------------- 1 | new_nop = function() { 2 | new("nop") 3 | } 4 | 5 | #' @rdname nop 6 | #' @export 7 | nop = make_operation("nop", new_nop) 8 | 9 | 10 | # operation application --------------------------------------------------- 11 | 12 | setMethod("apply_operation", signature(operation = "nop"), function(operation, layers) { 13 | layers 14 | }) 15 | 16 | 17 | # operation multiplication ------------------------------------------------ 18 | 19 | #' @rdname operation_product 20 | #' @export 21 | setMethod("*", signature(e1 = "nop", e2 = "nop"), function(e1, e2) e1) 22 | 23 | #' @rdname operation_product 24 | #' @export 25 | setMethod("*", signature(e1 = "operation", e2 = "nop"), function(e1, e2) e1) 26 | 27 | #' @rdname operation_product 28 | #' @export 29 | setMethod("*", signature(e1 = "operation_sum", e2 = "nop"), function(e1, e2) e1) 30 | 31 | #' @rdname operation_product 32 | #' @export 33 | setMethod("*", signature(e1 = "nop", e2 = "operation"), function(e1, e2) e2) 34 | 35 | #' @rdname operation_product 36 | #' @export 37 | setMethod("*", signature(e1 = "nop", e2 = "operation_sum"), function(e1, e2) e2) 38 | 39 | 40 | # printing ---------------------------------------------------------------- 41 | 42 | #' @rdname operation-class 43 | #' @export 44 | setMethod("format", signature(x = "nop"), function(x, ...) { 45 | "1" 46 | }) 47 | -------------------------------------------------------------------------------- /R/operation-partition.R: -------------------------------------------------------------------------------- 1 | #' Partition a layer into subgroups (Layer operation) 2 | #' 3 | #' A layer [operation] for adding a `partition` aesthetic to a [layer]. 4 | #' 5 | #' @param object One of: 6 | #' - A [layer-like] object: applies this operation to the layer. 7 | #' - A missing argument: creates an [operation] 8 | #' - Anything else: creates an [operation], passing `object` along to the 9 | #' `partition` argument 10 | #' @param partition One of: 11 | #' - A list of quosures, such as returned by [vars()], giving a (possibly multi-) 12 | #' column expression for the `partition` aesthetic. These expressions are 13 | #' combined using [interaction()] to be passed on to `aes(partition = ...)` 14 | #' - A one-sided formula, giving a single-column expression for the `partition` 15 | #' aesthetic, which is passed on to `aes_(partition = ...)`. 16 | #' @template operation 17 | #' 18 | #' @details 19 | #' This is a shortcut for setting the `partition` aesthetic of a [layer]. 20 | #' 21 | #' - `partition(~ XXX)` is roughly equivalent to `adjust(aes(partition = XXX))` 22 | #' - `partition(vars(X, Y, ...))` is roughly equivalent to `adjust(aes(partition = interaction(X, Y, ...)))` 23 | #' 24 | #' When a [layer] with a `partition` aesthetic is used by the following 25 | #' [operation]s, the effects of the operations are applied across groups: 26 | #' 27 | #' - [blend()]: Blends graphical objects within the subgroups defined by the 28 | #' partition together using normal (`"over"`) blending before applying its 29 | #' blend between subgroups. 30 | #' 31 | #' @examples 32 | #' \dontshow{old_options = options(ggblend.check_blend = FALSE)} 33 | #' library(ggplot2) 34 | #' 35 | #' # create two versions of a dataset, where draw order can affect output 36 | #' set.seed(1234) 37 | #' df_a = data.frame(x = rnorm(500, 0), y = rnorm(500, 1), set = "a") 38 | #' df_b = data.frame(x = rnorm(500, 1), y = rnorm(500, 2), set = "b") 39 | #' df_ab = rbind(df_a, df_b) |> 40 | #' transform(order = "draw a then b") 41 | #' df_ba = rbind(df_b, df_a) |> 42 | #' transform(order = "draw b then a") 43 | #' df = rbind(df_ab, df_ba) 44 | #' 45 | #' # Using the "multiply" blend mode, draw order does not matter, but 46 | #' # the "multiply" blend is applied to all points, creating dark regions 47 | #' # outside the intersection: 48 | #' df |> 49 | #' ggplot(aes(x, y, color = set)) + 50 | #' geom_point(size = 3, alpha = 0.5) |> blend("multiply") + 51 | #' scale_color_brewer(palette = "Set1") + 52 | #' facet_grid(~ order) 53 | #' 54 | #' # By partitioning (either through |> partition(vars(set)) or aes(partition = set)) 55 | #' # we will blend using the default blend mode (over) first, then we can apply the 56 | #' # "multiply" blend just between the two sets, so the regions outside the 57 | #' # intersection are not blended using "multiply": 58 | #' df |> 59 | #' ggplot(aes(x, y, color = set, partition = set)) + 60 | #' geom_point(size = 3, alpha = 0.5) |> blend("multiply") + 61 | #' scale_color_brewer(palette = "Set1") + 62 | #' facet_grid(~ order) 63 | #' \dontshow{options(old_options)} 64 | #' @name partition 65 | NULL 66 | 67 | new_partition = function(partition) { 68 | if (inherits(partition, "quosures")) { 69 | if (length(partition) > 1) { 70 | mapping = aes(partition = interaction(!!!partition, drop = TRUE, lex.order = TRUE)) 71 | } else { 72 | mapping = aes(partition = !!partition[[1]]) 73 | } 74 | } else if (inherits(partition, "formula")) { 75 | mapping = aes(partition = !!rlang::as_quosure(partition)) 76 | } else { 77 | mapping = aes(partition = !!partition) 78 | } 79 | 80 | adjust(mapping = mapping) 81 | } 82 | 83 | #' @rdname partition 84 | #' @export 85 | partition = make_operation("partition", new_partition, partition) 86 | -------------------------------------------------------------------------------- /R/operation-product.R: -------------------------------------------------------------------------------- 1 | new_operation_product = function(list) { 2 | new("operation_product", .Data = list) 3 | } 4 | 5 | #' @rdname operation_product 6 | #' @export 7 | setMethod("prod", signature(x = "operation"), function(x, ..., na.rm = FALSE) { 8 | new_operation_product(list(x, ...)) 9 | }) 10 | 11 | 12 | # type conversion --------------------------------------------------------- 13 | 14 | setAs("list", "operation_product", function(from) { 15 | new_operation_product(lapply(from, as, "operation")) 16 | }) 17 | 18 | setAs("operation", "operation_product", function(from) new_operation_product(list(from))) 19 | 20 | 21 | # operation application --------------------------------------------------- 22 | 23 | setMethod("apply_operation", signature(operation = "operation_product"), function(operation, layers) { 24 | Reduce(`*`, operation@.Data, layers) 25 | }) 26 | 27 | 28 | # operation multiplication ------------------------------------------------- 29 | 30 | #' @rdname operation_product 31 | #' @export 32 | setMethod("*", signature(e1 = "operation", e2 = "operation"), function(e1, e2) { 33 | e1 = as(e1, "operation_product") 34 | e2 = as(e2, "operation_product") 35 | new_operation_product(c(e1, e2)) 36 | }) 37 | 38 | #' @rdname operation_product 39 | #' @export 40 | setMethod("*", signature(e1 = "numeric", e2 = "operation"), function(e1, e2) { 41 | e2 = as(e2, "operation_product") 42 | operations = rep(e2@.Data, times = e1) 43 | new_operation_sum(operations) 44 | }) 45 | 46 | #' @rdname operation_product 47 | #' @export 48 | setMethod("*", signature(e1 = "operation", e2 = "numeric"), function(e1, e2) { 49 | e2 * e1 50 | }) 51 | 52 | #' @rdname operation_product 53 | #' @export 54 | setMethod("*", signature(e1 = "operation", e2 = "operation_sum"), function(e1, e2) { 55 | new_operation_sum(lapply(e2, `*`, e1 = e1)) 56 | }) 57 | 58 | #' @rdname operation_product 59 | #' @export 60 | setMethod("*", signature(e1 = "operation_sum", e2 = "operation"), function(e1, e2) { 61 | new_operation_sum(lapply(e1, `*`, e2 = e2)) 62 | }) 63 | 64 | #' @rdname operation_product 65 | #' @export 66 | setMethod("*", signature(e1 = "operation_sum", e2 = "operation_sum"), function(e1, e2) { 67 | new_operation_sum(do.call(c, lapply(e1, `*`, e2 = e2))) 68 | }) 69 | 70 | 71 | # printing ---------------------------------------------------------------- 72 | 73 | #' @rdname operation-class 74 | #' @export 75 | setMethod("format", signature(x = "operation_product"), function(x, ...) { 76 | if (length(x) == 0) { 77 | "0" 78 | } else { 79 | paste(vapply(x, format, character(1)), collapse = " * ") 80 | } 81 | }) 82 | -------------------------------------------------------------------------------- /R/operation-sum.R: -------------------------------------------------------------------------------- 1 | new_operation_sum = function(list) { 2 | new("operation_sum", .Data = list) 3 | } 4 | 5 | #' @rdname operation_sum 6 | #' @export 7 | setMethod("sum", signature(x = "operation"), function(x, ..., na.rm = FALSE) { 8 | as(list(x, ...), "operation_sum") 9 | }) 10 | 11 | 12 | # type conversion --------------------------------------------------------- 13 | 14 | setAs("list", "operation_sum", function(from) { 15 | new_operation_sum(do.call(c, lapply(from, as, "operation"))) 16 | }) 17 | 18 | setAs("operation", "operation_sum", function(from) new_operation_sum(list(from))) 19 | 20 | 21 | # operation application --------------------------------------------------- 22 | 23 | setMethod("apply_operation", signature(operation = "operation_sum"), function(operation, layers) { 24 | layer_apply(layers, function(layer) { 25 | lapply(operation, apply_operation, layers = layer) 26 | }) 27 | }) 28 | 29 | 30 | # operation concatenation ------------------------------------------------- 31 | 32 | #' @rdname operation_sum 33 | #' @export 34 | setMethod("+", signature(e1 = "operation", e2 = "operation"), function(e1, e2) { 35 | e1 = as(e1, "operation_sum") 36 | e2 = as(e2, "operation_sum") 37 | new_operation_sum(c(e1, e2)) 38 | }) 39 | 40 | #' @rdname operation_sum 41 | #' @export 42 | setMethod("+", signature(e1 = "operation", e2 = "numeric"), function(e1, e2) { 43 | e1 + e2 * nop() 44 | }) 45 | 46 | #' @rdname operation_sum 47 | #' @export 48 | setMethod("+", signature(e1 = "numeric", e2 = "operation"), function(e1, e2) { 49 | e1 * nop() + e2 50 | }) 51 | 52 | 53 | # printing ---------------------------------------------------------------- 54 | 55 | #' @rdname operation_sum 56 | #' @export 57 | setMethod("format", signature(x = "operation_sum"), function(x, ...) { 58 | if (length(x) == 0) { 59 | "0" 60 | } else if (length(x) == 1) { 61 | format(x[[1]]) 62 | } else { 63 | paste0("(", paste(vapply(x, format, character(1)), collapse = " + "), ")") 64 | } 65 | }) 66 | -------------------------------------------------------------------------------- /R/transform.R: -------------------------------------------------------------------------------- 1 | # transforming layers and grobs ------------------------------------------- 2 | 3 | #' Transform input grobs as one 4 | #' @param ... grobs to transform 5 | #' @param grob_transform a function that transforms a grob 6 | #' @return a grob containing transformed versions of all input grobs 7 | #' @noRd 8 | transform_grobs = function(..., grob_transform) { 9 | grob_transform(grobTree(...)) 10 | } 11 | 12 | #' Transform groblists together. Each groblist represents the grobs for a layer, 13 | #' and is lists of grobs, one for each panel. 14 | #' @param groblists a list of groblists. Each groblist is a list of grobs. 15 | #' @param grob_transform a function that takes a grob and returns a transformed grob 16 | #' @return a single groblist 17 | #' @noRd 18 | transform_groblists = function(groblists, grob_transform) { 19 | groblist = .mapply(transform_grobs, groblists, list(grob_transform = grob_transform)) 20 | if (length(groblists) > 0) { 21 | names(groblist) = names(groblists[[1]]) 22 | } 23 | groblist 24 | } 25 | 26 | #' transform layers 27 | #' @param groblists a list of ggplot2::Layers. Should be hidden layers (as 28 | #' returned by hidden_layer()). 29 | #' @param grob_transform a function taking a grob and returning a transformed grob 30 | #' @param check a function to call to check if this transformation is valid (and 31 | #' throw a warning if necessary) 32 | #' @return a ggplot2::Layer 33 | #' @noRd 34 | transform_layers = function(layers, grob_transform, check = function() NULL) { 35 | force(layers) 36 | force(grob_transform) 37 | force(check) 38 | 39 | # skip over hidden layers when transforming (they should already be incorporated 40 | # into a TransformedLayer) and elements that aren't layers (e.g. coords, scales, etc) 41 | layers = flatten_layer_list(layers) 42 | to_transform = 43 | vapply(layers, inherits, what = "LayerInstance", logical(1)) & 44 | !vapply(layers, inherits, what = "HiddenLayer", logical(1)) 45 | layers[to_transform] = lapply(layers[to_transform], hide_layer) 46 | layers_to_transform = layers[to_transform] 47 | 48 | transformed_layer = ggproto("TransformedLayer", geom_blank(inherit.aes = FALSE), 49 | draw_geom = function(self, data, layout) { 50 | check() 51 | groblists = lapply(layers_to_transform, function(l) { 52 | groblist = l$ggblend__draw_geom_(layout) 53 | # do not transform within layers 54 | lapply(groblist, groupGrob) 55 | }) 56 | transform_groblists(groblists, grob_transform) 57 | } 58 | ) 59 | 60 | c(layers, list(transformed_layer)) 61 | } 62 | 63 | #' Make a layer that will transform its contents when created. If the layer does not 64 | #' have a partition aesthetic, transform all its grobs at once. If it does, 65 | #' first generate grobs for each partition, then transform the groups. 66 | #' @param layer a ggplot2::Layer 67 | #' @param grob_transform a function taking a grob and returning a transformed grob 68 | #' @noRd 69 | transform_layer = function(layer, grob_transform, check = function() NULL) { 70 | force(layer) 71 | force(grob_transform) 72 | force(check) 73 | 74 | ggproto(NULL, layer, 75 | draw_geom = function(self, data, layout) { 76 | check() 77 | if (is.null(data$partition)) { 78 | # absent a partition aes, we apply the transform to all grobs in the layer 79 | groblist = ggproto_parent(layer, self)$draw_geom(data, layout) 80 | lapply(groblist, grob_transform) 81 | } else { 82 | # with a partition aes, we apply the transform over partitions 83 | 84 | # draw the geom for each partition separately 85 | # groblists will be a list of groblists, where each groblist is a list 86 | # of grobs for a single layer 87 | groblists = lapply(split(data, data$partition), function(d) { 88 | groblist = ggproto_parent(layer, self)$draw_geom(d, layout) 89 | # make layers their own group so that the transform is only 90 | # applied between layers, not within layers 91 | lapply(groblist, groupGrob) 92 | }) 93 | 94 | transform_groblists(groblists, grob_transform) 95 | } 96 | } 97 | ) 98 | } 99 | 100 | #' Make a hidden layer that does not draw itself by default. We will draw the 101 | #' layer ourselves when applying transforms 102 | #' @param a ggplot2::Layer to hide 103 | #' @return a ggplot2::Layer whose `$draw_geom()` method does not draw, but 104 | #' saves data for later drawing. Use `$ggblend__draw_geom_()` later to draw. 105 | #' @noRd 106 | hide_layer = function(layer) { 107 | force(layer) 108 | store = environment() 109 | 110 | ggproto("HiddenLayer", layer, 111 | # keep ggplot from drawing this layer normally, we will draw it later 112 | draw_geom = function(self, data, layout) { 113 | store$last_grobs = ggproto_parent(layer, self)$draw_geom(data, layout) 114 | 115 | # draw nothing here 116 | rep(list(zeroGrob()), nrow(layout$layout)) 117 | }, 118 | # function to actually draw the geom, using the grobs saved from the last 119 | # call to $draw_geom() 120 | ggblend__draw_geom_ = function(self, layout) { 121 | store$last_grobs 122 | } 123 | ) 124 | } 125 | -------------------------------------------------------------------------------- /R/util.R: -------------------------------------------------------------------------------- 1 | stop0 = function(...) { 2 | stop(..., call. = FALSE) 3 | } 4 | 5 | warning0 = function(...) { 6 | warning(..., call. = FALSE) 7 | } 8 | 9 | cat0 = function(...) { 10 | cat(..., sep = "") 11 | } 12 | 13 | format_name_value_pairs = function(x, defaults = list()) { 14 | # remove values that are set to their defaults or to NULL 15 | x = unclass(x) 16 | x[as.logical(mapply(identical, x, defaults[names(x)]))] = NULL 17 | 18 | if (length(x) > 0) { 19 | paste0( 20 | names(x), " = ", 21 | vapply(x, function(x) if (is.character(x)) deparse1(x) else format(x), character(1)), 22 | collapse = ", " 23 | ) 24 | } 25 | } 26 | 27 | bullet = function(x) { 28 | paste0(strwrap(paste0("- ", x), exdent = 3, indent = 1), collapse = "\n") 29 | } 30 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | --- 4 | 5 | ```{r, include = FALSE} 6 | knitr::opts_chunk$set( 7 | fig.path = "man/figures/README-", 8 | dev.args = list(png = list(type = "cairo")), 9 | fig.retina = 2 10 | ) 11 | ``` 12 | 13 | # ggblend: Blending and compositing algebra for ggplot2 14 | 15 | 16 | [![Lifecycle: experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html#experimental) 17 | [![CRAN status](https://www.r-pkg.org/badges/version/ggblend)](https://CRAN.R-project.org/package=ggblend) 18 | [![Codecov test coverage](https://codecov.io/gh/mjskay/ggblend/branch/main/graph/badge.svg)](https://app.codecov.io/gh/mjskay/ggblend?branch=main) 19 | [![R-CMD-check](https://github.com/mjskay/ggblend/workflows/R-CMD-check/badge.svg)](https://github.com/mjskay/ggblend/actions) 20 | [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.7963886.svg)](https://doi.org/10.5281/zenodo.7963886) 21 | 22 | 23 | *ggblend* is a small algebra of operations for blending, copying, adjusting, and 24 | compositing layers in *ggplot2*. It allows you to easily copy and adjust the 25 | aesthetics or parameters of an existing layer, to partition a layer into multiple 26 | pieces for re-composition, and to combine layers (or partitions of layers) using 27 | blend modes (like `"multiply"`, `"overlay"`, etc). 28 | 29 | *ggblend* requires R ≥ 4.2, as blending and compositing support was added in that 30 | version of R. 31 | 32 | ## Installation 33 | 34 | You can install *ggblend* from CRAN as follows: 35 | 36 | ```r 37 | install.packages("ggblend") 38 | ``` 39 | 40 | You can install the development version of *ggblend* using: 41 | 42 | ```r 43 | remotes::install_github("mjskay/ggblend") 44 | ``` 45 | 46 | ## Blending within one geometry 47 | 48 | We'll construct a simple dataset with two semi-overlapping point clouds. We'll 49 | have two versions of the dataset: one with all the `"a"` points listed first, 50 | and one with all the `"b"` points listed first. 51 | 52 | ```{r data, message=FALSE, warning=FALSE} 53 | library(ggplot2) 54 | library(ggblend) 55 | theme_set(ggdist::theme_ggdist() + theme( 56 | plot.title = element_text(size = rel(1), lineheight = 1.1, face = "bold"), 57 | plot.subtitle = element_text(face = "italic"), 58 | panel.border = element_rect(color = "gray75", fill = NA) 59 | )) 60 | 61 | set.seed(1234) 62 | df_a = data.frame(x = rnorm(500, 0), y = rnorm(500, 1), set = "a") 63 | df_b = data.frame(x = rnorm(500, 1), y = rnorm(500, 2), set = "b") 64 | 65 | df_ab = rbind(df_a, df_b) |> 66 | transform(order = "draw a then b") 67 | 68 | df_ba = rbind(df_b, df_a) |> 69 | transform(order = "draw b then a") 70 | 71 | df = rbind(df_ab, df_ba) 72 | ``` 73 | 74 | A typical scatterplot of such data suffers from the problem that how many 75 | points appear to be in each group depends on the drawing order (*a then b* 76 | versus *b then a*): 77 | 78 | ```{r scatter_noblend} 79 | df |> 80 | ggplot(aes(x, y, color = set)) + 81 | geom_point(size = 3, alpha = 0.5) + 82 | scale_color_brewer(palette = "Set1") + 83 | facet_grid(~ order) + 84 | labs(title = "geom_point() without blending", subtitle = "Draw order matters.") 85 | ``` 86 | 87 | A *commutative* blend mode, like `"multiply"` or `"darken"`, is one potential 88 | solution that does not depend on drawing order. We can apply a `blend()` 89 | operation to geom_point()` to achieve this. There three ways to do this: 90 | 91 | - `blend(geom_point(...), "multiply")` (normal function application) 92 | - `geom_point(...) |> blend("multiply")` (piping) 93 | - `geom_point(...) * blend("multiply")` (algebraic operations) 94 | 95 | Function application and piping are equivalent. **In this case**, all three 96 | approaches are equivalent. As we will see later, the multiplication approach 97 | is useful when we want a shorthand for applying the same operation to multiple 98 | layers in a list without combining those layers first (in other words, 99 | multiplication of operations over layers is *distributive* in an algebraic 100 | sense). 101 | 102 | ```{r scatter_blend} 103 | df |> 104 | ggplot(aes(x, y, color = set)) + 105 | geom_point(size = 3, alpha = 0.5) |> blend("multiply") + 106 | scale_color_brewer(palette = "Set1") + 107 | facet_grid(~ order) + 108 | labs( 109 | title = "geom_point(alpha = 0.5) |> blend('multiply')", 110 | subtitle = "Draw order does not matter, but color is too dark." 111 | ) 112 | ``` 113 | 114 | Now the output is identical no matter the draw order, although the output is quite dark. 115 | 116 | ## Partitioning layers 117 | 118 | Part of the reason the output is very dark above is that all of the points are being 119 | multiply-blended together. When many objects (here, individual points) are multiply-blended on top of each 120 | other, the output tends to get dark very quickly. 121 | 122 | However, we really only need the two sets to be multiply-blended with each other. 123 | Within each set, we can use regular alpha blending. To do that, we can partition the geometry 124 | by `set` and then blend. Each partition will be blended normally within the set, and 125 | then the resulting sets will be multiply-blended together just once: 126 | 127 | ```{r scatter_partition_blend} 128 | df |> 129 | ggplot(aes(x, y, color = set)) + 130 | geom_point(size = 3, alpha = 0.5) |> partition(vars(set)) |> blend("multiply") + 131 | scale_color_brewer(palette = "Set1") + 132 | facet_grid(~ order) + 133 | labs( 134 | title = "geom_point(alpha = 0.5) |> partition(vars(set)) |> blend('multiply')", 135 | subtitle = "Light outside the intersection, but still dark inside the intersection." 136 | ) 137 | ``` 138 | 139 | That's getting there: points outside the intersection of the two sets look good, 140 | but the intersection is still a bit dark. 141 | 142 | Let's try combining two blend modes to address this: we'll use a `"lighten"` 143 | blend mode (which is also commutative) to make the overlapping regions 144 | lighter, and then draw the `"multiply"`-blended version on top at an `alpha` 145 | of less than 1: 146 | 147 | ```{r scatter_lighten_multiply} 148 | df |> 149 | ggplot(aes(x, y, color = set)) + 150 | geom_point(size = 3, alpha = 0.5) |> partition(vars(set)) |> blend("lighten") + 151 | geom_point(size = 3, alpha = 0.5) |> partition(vars(set)) |> blend("multiply", alpha = 0.5) + 152 | scale_color_brewer(palette = "Set1") + 153 | facet_grid(~ order) + 154 | labs( 155 | title = 156 | "geom_point(size = 3, alpha = 0.5) |> partition(vars(set)) |> blend('lighten') + \ngeom_point(size = 3, alpha = 0.5) |> partition(vars(set)) |> blend('multiply', alpha = 0.5)", 157 | subtitle = 'A good compromise, but a long specification.' 158 | ) + 159 | theme(plot.subtitle = element_text(lineheight = 1.2)) 160 | ``` 161 | 162 | Now it's a little easier to see both overlap and density, and the output remains independent of draw order. 163 | 164 | However, it is a little verbose to need to copy out a layer multiple times: 165 | 166 | ```r 167 | geom_point(size = 3, alpha = 0.5) |> partition(vars(set)) * blend("lighten") + 168 | geom_point(size = 3, alpha = 0.5) |> partition(vars(set)) * blend("multiply", alpha = 0.5) + 169 | ``` 170 | 171 | We can simplify this is two ways: first, `partition(vars(set))` is equivalent 172 | to setting `aes(partition = set)`, so we can move the partition specification 173 | into the global plot aesthetics, since it is the same on every layer. 174 | 175 | Second, operations and layers in *ggblend* act as a small algebra. Operations and sums 176 | of operations can be multiplied by layers and lists of layers, and those 177 | operations are distributed over the layers (This is where `*` and `|>` differ: 178 | `|>` does not distribute operations like `blend()` over layers, which is 179 | useful if you want to use a blend to combine multiple layers together, rather 180 | than applying that blend to each layer individually). 181 | 182 | Thus, we can "factor out" 183 | `geom_point(size = 3, alpha = 0.5)` from the above expression, yielding this: 184 | 185 | ```r 186 | geom_point(size = 3, alpha = 0.5) * (blend("lighten") + blend("multiply", alpha = 0.5)) 187 | ``` 188 | 189 | Both expressions are equivalent. Thus we can rewrite the previous example 190 | like so: 191 | 192 | ```{r scatter_lighten_multiply_stacked} 193 | df |> 194 | ggplot(aes(x, y, color = set, partition = set)) + 195 | geom_point(size = 3, alpha = 0.5) * (blend("lighten") + blend("multiply", alpha = 0.5)) + 196 | scale_color_brewer(palette = "Set1") + 197 | facet_grid(~ order) + 198 | labs( 199 | title = "geom_point(aes(partition = set)) * (blend('lighten') + blend('multiply', alpha = 0.5))", 200 | subtitle = "Two order-independent blends on one layer using the distributive law." 201 | ) + 202 | theme(plot.subtitle = element_text(lineheight = 1.2)) 203 | ``` 204 | 205 | ## Blending multiple geometries 206 | 207 | We can also blend geometries together by passing a list of geometries to `blend()`. 208 | These lists can include already-blended geometries: 209 | 210 | ```{r scatter_blend_geom_incorrect} 211 | df |> 212 | ggplot(aes(x, y, color = set, partition = set)) + 213 | list( 214 | geom_point(size = 3, alpha = 0.5) * (blend("lighten") + blend("multiply", alpha = 0.5)), 215 | geom_vline(xintercept = 0, color = "gray75", linewidth = 1.5), 216 | geom_hline(yintercept = 0, color = "gray75", linewidth = 1.5) 217 | ) |> blend("hard.light") + 218 | scale_color_brewer(palette = "Set1") + 219 | facet_grid(~ order) + 220 | labs( 221 | title = "Blending multiple geometries together in a list", 222 | subtitle = "Careful! The point layer blend is incorrect!" 223 | ) 224 | ``` 225 | 226 | Whoops!! If you look closely, the blending of the `geom_point()` layers appears to 227 | have changed. Recall that this expression: 228 | 229 | ```r 230 | geom_point(size = 3, alpha = 0.5) * (blend("lighten") + blend("multiply", alpha = 0.5)) 231 | ``` 232 | 233 | Is equivalent to specifying two separate layers, one with `blend("lighten")` 234 | and the other with `blend("multiply", alpha = 0.65))`. Thus, when you apply 235 | `|> blend("hard.light")` to the `list()` of layers, it will use a hard light 236 | blend mode to blend these two layers together, when previously they would be 237 | blended using the normal (or `"over"`) blend mode. 238 | 239 | We can gain back the original appearance by blending these two layers together 240 | with `|> blend()` prior to applying the hard light blend: 241 | 242 | ```{r scatter_blend_geom} 243 | df |> 244 | ggplot(aes(x, y, color = set, partition = set)) + 245 | list( 246 | geom_point(size = 3, alpha = 0.5) * (blend("lighten") + blend("multiply", alpha = 0.5)) |> blend(), 247 | geom_vline(xintercept = 0, color = "gray75", linewidth = 1.5), 248 | geom_hline(yintercept = 0, color = "gray75", linewidth = 1.5) 249 | ) |> blend("hard.light") + 250 | scale_color_brewer(palette = "Set1") + 251 | facet_grid(~ order) + 252 | labs(title = "Blending multiple geometries together") 253 | ``` 254 | 255 | 256 | ## Partitioning and blending lineribbons 257 | 258 | Another case where it's useful to have finer-grained control of blending within a given 259 | geometry is when drawing overlapping uncertainty bands. Here, we'll show how to use `blend()` with `stat_lineribbon()` 260 | from [ggdist](https://mjskay.github.io/ggdist/) 261 | to create overlapping gradient ribbons depicting uncertainty. 262 | 263 | We'll fit a model: 264 | 265 | ```{r m_mpg} 266 | m_mpg = lm(mpg ~ hp * cyl, data = mtcars) 267 | ``` 268 | 269 | And generate some confidence distributions for the mean using [distributional](https://pkg.mitchelloharawild.com/distributional/): 270 | 271 | ```{r lineribbon} 272 | predictions = unique(mtcars[, c("cyl", "hp")]) 273 | 274 | predictions$mu_hat = with(predict(m_mpg, newdata = predictions, se.fit = TRUE), 275 | distributional::dist_student_t(df = df, mu = fit, sigma = se.fit) 276 | ) 277 | 278 | predictions 279 | ``` 280 | 281 | A basic plot based on examples in `vignette("freq-uncertainty-vis", package = "ggdist")` and 282 | `vignette("lineribbon", package = "ggdist")` may have issues when lineribbons overlap: 283 | 284 | ```{r lineribbon_noblend} 285 | predictions |> 286 | ggplot(aes(x = hp, fill = ordered(cyl), color = ordered(cyl))) + 287 | ggdist::stat_lineribbon( 288 | aes(ydist = mu_hat, fill_ramp = after_stat(.width)), 289 | .width = ppoints(40) 290 | ) + 291 | geom_point(aes(y = mpg), data = mtcars) + 292 | scale_fill_brewer(palette = "Set2") + 293 | scale_color_brewer(palette = "Dark2") + 294 | ggdist::scale_fill_ramp_continuous(range = c(1, 0)) + 295 | labs( 296 | title = "ggdist::stat_lineribbon()", 297 | subtitle = "Overlapping lineribbons obscure each other.", 298 | color = "cyl", fill = "cyl", y = "mpg" 299 | ) 300 | ``` 301 | 302 | Notice the overlap of the orange (`cyl = 6`) and purple (`cyl = 8`) lines. 303 | 304 | If we add a `partition = cyl` aesthetic mapping, we can blend the geometries 305 | for the different levels of `cyl` together with a `blend()` call around 306 | `ggdist::stat_lineribbon()`. 307 | 308 | There are many ways we could add the partition to the plot: 309 | 310 | 1. Add `partition = cyl` to the existing `aes(...)` call. However, this 311 | leaves the partitioning information far from the call to `blend()`, so the 312 | relationship between them is less clear. 313 | 2. Add `aes(partition = cyl)` to the `stat_lineribbon(...)` call. This is 314 | a more localized change (better!), but will raise a warning if `stat_lineribbon()` 315 | itself does not recognized the `partition` aesthetic. 316 | 3. Add `|> adjust(aes(partition = cyl))` after `stat_lineribbon(...)` to 317 | add the `partition` aesthetic to it (this will bypass the warning). 318 | 4. Add `|> partition(vars(cyl))` after `stat_lineribbon(...)` to add the 319 | `partition` aesthetic. This is an alias for the `adjust()` approach that is 320 | intended to be clearer. It takes a specification for a partition that is 321 | similar to `facet_wrap()`: either a one-sided formula or a call to `vars()`. 322 | 323 | Let's try the fourth approach: 324 | 325 | ```{r lineribbon_blend} 326 | predictions |> 327 | ggplot(aes(x = hp, fill = ordered(cyl), color = ordered(cyl))) + 328 | ggdist::stat_lineribbon( 329 | aes(ydist = mu_hat, fill_ramp = after_stat(.width)), 330 | .width = ppoints(40) 331 | ) |> partition(vars(cyl)) |> blend("multiply") + 332 | geom_point(aes(y = mpg), data = mtcars) + 333 | scale_fill_brewer(palette = "Set2") + 334 | scale_color_brewer(palette = "Dark2") + 335 | ggdist::scale_fill_ramp_continuous(range = c(1, 0)) + 336 | labs( 337 | title = "ggdist::stat_lineribbon() |> partition(vars(cyl)) |> blend('multiply')", 338 | subtitle = "Overlapping lineribbons blend together independent of draw order.", 339 | color = "cyl", fill = "cyl", y = "mpg" 340 | ) 341 | ``` 342 | 343 | Now the overlapping ribbons are blended together. 344 | 345 | ## Highlighting geoms using `copy_under()` 346 | 347 | A common visualization technique to make a layer more salient (especially in the 348 | presence of many other competing layers) is to add a small outline around 349 | it. For some geometries (like `geom_point()`) this is easy; but for others (like `geom_line()`), 350 | there's no easy way to do this without manually copying the layer. 351 | 352 | The *ggblend* layer algebra makes this straightforward using the `adjust()` operation 353 | combined with operator addition and multiplication. For example, given a layer 354 | like: 355 | 356 | ```r 357 | geom_line(linewidth = 1) 358 | ``` 359 | 360 | To add a white outline, you might want something like: 361 | 362 | ```r 363 | geom_line(color = "white", linewidth = 2.5) + geom_line(linewidth = 1) 364 | ``` 365 | 366 | However, we'd rather not have to write the `geom_line()` specification twice 367 | If we factor out the differences between the first and second layer, we can use 368 | the `adjust()` operation (which lets you change the aesthetics and parameters 369 | of a layer) along with the distributive law to factor out 370 | `geom_line(linewidth = 1)` and write the above specification as: 371 | 372 | ```r 373 | geom_line(linewidth = 1) * (adjust(color = "white", linewidth = 2.5) + 1) 374 | ``` 375 | 376 | The `copy_under(...)` operation, which is a synonym for `adjust(...) + 1`, 377 | also implements this pattern: 378 | 379 | ```r 380 | geom_line(linewidth = 1) * copy_under(color = "white", linewidth = 2.5) 381 | ``` 382 | 383 | Here's an example highlighting the fit lines from our previous lineribbon example: 384 | 385 | ```{r lineribbon_blend_highlight} 386 | predictions |> 387 | ggplot(aes(x = hp, fill = ordered(cyl), color = ordered(cyl))) + 388 | ggdist::stat_ribbon( 389 | aes(ydist = mu_hat, fill_ramp = after_stat(.width)), 390 | .width = ppoints(40) 391 | ) |> partition(vars(cyl)) |> blend("multiply") + 392 | geom_line(aes(y = median(mu_hat)), linewidth = 1) |> copy_under(color = "white", linewidth = 2.5) + 393 | geom_point(aes(y = mpg), data = mtcars) + 394 | scale_fill_brewer(palette = "Set2") + 395 | scale_color_brewer(palette = "Dark2") + 396 | ggdist::scale_fill_ramp_continuous(range = c(1, 0)) + 397 | labs( 398 | title = "geom_line() |> copy_under(color = 'white', linewidth = 2.5)", 399 | subtitle = "Highlights the line layer without manually copying its specification.", 400 | color = "cyl", fill = "cyl", y = "mpg" 401 | ) 402 | ``` 403 | 404 | Note that the implementation of `copy_under(...)` is simply a synonym for 405 | `adjust(...) + 1`; we can see this if we look at `copy_under()` itself: 406 | 407 | ```{r} 408 | copy_under() 409 | ``` 410 | 411 | In fact, not that it is particularly useful, but addition and multiplication 412 | of layer operations is expanded appropriately: 413 | 414 | ```{r} 415 | (adjust() + 3) * 2 416 | ``` 417 | 418 | I hesitate to imagine what that feature might be useful for... 419 | 420 | ## Compatibility with other packages 421 | 422 | In theory *ggblend* should be compatible with other packages, though in more 423 | complex cases (blending lists of geoms or using the `partition` aesthetic) 424 | it is possible it may fail, as these features are a bit more hackish. I have 425 | done some testing with a few other layer-manipulating packages---including 426 | [gganimate](https://gganimate.com/), [ggnewscale](https://eliocamp.github.io/ggnewscale/), 427 | and [relayer](https://github.com/clauswilke/relayer)---and they appear to be 428 | compatible. 429 | 430 | As a hard test, here is all three features applied to a modified version of the 431 | Gapminder example used in the [gganimate documentation](https://gganimate.com/): 432 | 433 | ```{r gapminder, message=FALSE, warning=FALSE} 434 | library(gganimate) 435 | library(gapminder) 436 | 437 | p = gapminder |> 438 | ggplot(aes(gdpPercap, lifeExp, size = pop, color = continent)) + 439 | list( 440 | geom_point(show.legend = c(size = FALSE)) |> partition(vars(continent)) |> blend("multiply"), 441 | geom_hline(yintercept = 70, linewidth = 1.5, color = "gray75") 442 | ) |> blend("hard.light") + 443 | scale_color_manual( 444 | # same as colorspace::lighten(continent_colors, 0.35) 445 | values = c( 446 | Africa = "#BE7658", Americas = "#E95866", Asia = "#7C5C86", 447 | Europe = "#659C5D", Oceania = "#7477CA" 448 | ), 449 | guide = guide_legend(override.aes = list(size = 4)) 450 | ) + 451 | scale_size(range = c(2, 12)) + 452 | scale_x_log10(labels = scales::label_dollar(scale_cut = scales::cut_short_scale())) + 453 | scale_y_continuous(breaks = seq(20, 80, by = 10)) + 454 | labs( 455 | title = 'Gapminder with gganimate and ggblend', 456 | subtitle = 'Year: {frame_time}', 457 | x = 'GDP per capita', 458 | y = 'Life expectancy' 459 | ) + 460 | transition_time(year) + 461 | ease_aes('linear') 462 | 463 | animate(p, type = "cairo", width = 600, height = 400, res = 100) 464 | ``` 465 | 466 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # ggblend: Blending and compositing algebra for ggplot2 3 | 4 | 5 | 6 | [![Lifecycle: 7 | experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html#experimental) 8 | [![CRAN 9 | status](https://www.r-pkg.org/badges/version/ggblend)](https://CRAN.R-project.org/package=ggblend) 10 | [![Codecov test 11 | coverage](https://codecov.io/gh/mjskay/ggblend/branch/main/graph/badge.svg)](https://app.codecov.io/gh/mjskay/ggblend?branch=main) 12 | [![R-CMD-check](https://github.com/mjskay/ggblend/workflows/R-CMD-check/badge.svg)](https://github.com/mjskay/ggblend/actions) 13 | [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.7963886.svg)](https://doi.org/10.5281/zenodo.7963886) 14 | 15 | 16 | *ggblend* is a small algebra of operations for blending, copying, 17 | adjusting, and compositing layers in *ggplot2*. It allows you to easily 18 | copy and adjust the aesthetics or parameters of an existing layer, to 19 | partition a layer into multiple pieces for re-composition, and to 20 | combine layers (or partitions of layers) using blend modes (like 21 | `"multiply"`, `"overlay"`, etc). 22 | 23 | *ggblend* requires R ≥ 4.2, as blending and compositing support was 24 | added in that version of R. 25 | 26 | ## Installation 27 | 28 | You can install *ggblend* from CRAN as follows: 29 | 30 | ``` r 31 | install.packages("ggblend") 32 | ``` 33 | 34 | You can install the development version of *ggblend* using: 35 | 36 | ``` r 37 | remotes::install_github("mjskay/ggblend") 38 | ``` 39 | 40 | ## Blending within one geometry 41 | 42 | We’ll construct a simple dataset with two semi-overlapping point clouds. 43 | We’ll have two versions of the dataset: one with all the `"a"` points 44 | listed first, and one with all the `"b"` points listed first. 45 | 46 | ``` r 47 | library(ggplot2) 48 | library(ggblend) 49 | theme_set(ggdist::theme_ggdist() + theme( 50 | plot.title = element_text(size = rel(1), lineheight = 1.1, face = "bold"), 51 | plot.subtitle = element_text(face = "italic"), 52 | panel.border = element_rect(color = "gray75", fill = NA) 53 | )) 54 | 55 | set.seed(1234) 56 | df_a = data.frame(x = rnorm(500, 0), y = rnorm(500, 1), set = "a") 57 | df_b = data.frame(x = rnorm(500, 1), y = rnorm(500, 2), set = "b") 58 | 59 | df_ab = rbind(df_a, df_b) |> 60 | transform(order = "draw a then b") 61 | 62 | df_ba = rbind(df_b, df_a) |> 63 | transform(order = "draw b then a") 64 | 65 | df = rbind(df_ab, df_ba) 66 | ``` 67 | 68 | A typical scatterplot of such data suffers from the problem that how 69 | many points appear to be in each group depends on the drawing order (*a 70 | then b* versus *b then a*): 71 | 72 | ``` r 73 | df |> 74 | ggplot(aes(x, y, color = set)) + 75 | geom_point(size = 3, alpha = 0.5) + 76 | scale_color_brewer(palette = "Set1") + 77 | facet_grid(~ order) + 78 | labs(title = "geom_point() without blending", subtitle = "Draw order matters.") 79 | ``` 80 | 81 | 82 | 83 | A *commutative* blend mode, like `"multiply"` or `"darken"`, is one 84 | potential solution that does not depend on drawing order. We can apply a 85 | `blend()` operation to geom_point()\` to achieve this. There three ways 86 | to do this: 87 | 88 | - `blend(geom_point(...), "multiply")` (normal function application) 89 | - `geom_point(...) |> blend("multiply")` (piping) 90 | - `geom_point(...) * blend("multiply")` (algebraic operations) 91 | 92 | Function application and piping are equivalent. **In this case**, all 93 | three approaches are equivalent. As we will see later, the 94 | multiplication approach is useful when we want a shorthand for applying 95 | the same operation to multiple layers in a list without combining those 96 | layers first (in other words, multiplication of operations over layers 97 | is *distributive* in an algebraic sense). 98 | 99 | ``` r 100 | df |> 101 | ggplot(aes(x, y, color = set)) + 102 | geom_point(size = 3, alpha = 0.5) |> blend("multiply") + 103 | scale_color_brewer(palette = "Set1") + 104 | facet_grid(~ order) + 105 | labs( 106 | title = "geom_point(alpha = 0.5) |> blend('multiply')", 107 | subtitle = "Draw order does not matter, but color is too dark." 108 | ) 109 | ``` 110 | 111 | 112 | 113 | Now the output is identical no matter the draw order, although the 114 | output is quite dark. 115 | 116 | ## Partitioning layers 117 | 118 | Part of the reason the output is very dark above is that all of the 119 | points are being multiply-blended together. When many objects (here, 120 | individual points) are multiply-blended on top of each other, the output 121 | tends to get dark very quickly. 122 | 123 | However, we really only need the two sets to be multiply-blended with 124 | each other. Within each set, we can use regular alpha blending. To do 125 | that, we can partition the geometry by `set` and then blend. Each 126 | partition will be blended normally within the set, and then the 127 | resulting sets will be multiply-blended together just once: 128 | 129 | ``` r 130 | df |> 131 | ggplot(aes(x, y, color = set)) + 132 | geom_point(size = 3, alpha = 0.5) |> partition(vars(set)) |> blend("multiply") + 133 | scale_color_brewer(palette = "Set1") + 134 | facet_grid(~ order) + 135 | labs( 136 | title = "geom_point(alpha = 0.5) |> partition(vars(set)) |> blend('multiply')", 137 | subtitle = "Light outside the intersection, but still dark inside the intersection." 138 | ) 139 | ``` 140 | 141 | 142 | 143 | That’s getting there: points outside the intersection of the two sets 144 | look good, but the intersection is still a bit dark. 145 | 146 | Let’s try combining two blend modes to address this: we’ll use a 147 | `"lighten"` blend mode (which is also commutative) to make the 148 | overlapping regions lighter, and then draw the `"multiply"`-blended 149 | version on top at an `alpha` of less than 1: 150 | 151 | ``` r 152 | df |> 153 | ggplot(aes(x, y, color = set)) + 154 | geom_point(size = 3, alpha = 0.5) |> partition(vars(set)) |> blend("lighten") + 155 | geom_point(size = 3, alpha = 0.5) |> partition(vars(set)) |> blend("multiply", alpha = 0.5) + 156 | scale_color_brewer(palette = "Set1") + 157 | facet_grid(~ order) + 158 | labs( 159 | title = 160 | "geom_point(size = 3, alpha = 0.5) |> partition(vars(set)) |> blend('lighten') + \ngeom_point(size = 3, alpha = 0.5) |> partition(vars(set)) |> blend('multiply', alpha = 0.5)", 161 | subtitle = 'A good compromise, but a long specification.' 162 | ) + 163 | theme(plot.subtitle = element_text(lineheight = 1.2)) 164 | ``` 165 | 166 | 167 | 168 | Now it’s a little easier to see both overlap and density, and the output 169 | remains independent of draw order. 170 | 171 | However, it is a little verbose to need to copy out a layer multiple 172 | times: 173 | 174 | ``` r 175 | geom_point(size = 3, alpha = 0.5) |> partition(vars(set)) * blend("lighten") + 176 | geom_point(size = 3, alpha = 0.5) |> partition(vars(set)) * blend("multiply", alpha = 0.5) + 177 | ``` 178 | 179 | We can simplify this is two ways: first, `partition(vars(set))` is 180 | equivalent to setting `aes(partition = set)`, so we can move the 181 | partition specification into the global plot aesthetics, since it is the 182 | same on every layer. 183 | 184 | Second, operations and layers in *ggblend* act as a small algebra. 185 | Operations and sums of operations can be multiplied by layers and lists 186 | of layers, and those operations are distributed over the layers (This is 187 | where `*` and `|>` differ: `|>` does not distribute operations like 188 | `blend()` over layers, which is useful if you want to use a blend to 189 | combine multiple layers together, rather than applying that blend to 190 | each layer individually). 191 | 192 | Thus, we can “factor out” `geom_point(size = 3, alpha = 0.5)` from the 193 | above expression, yielding this: 194 | 195 | ``` r 196 | geom_point(size = 3, alpha = 0.5) * (blend("lighten") + blend("multiply", alpha = 0.5)) 197 | ``` 198 | 199 | Both expressions are equivalent. Thus we can rewrite the previous 200 | example like so: 201 | 202 | ``` r 203 | df |> 204 | ggplot(aes(x, y, color = set, partition = set)) + 205 | geom_point(size = 3, alpha = 0.5) * (blend("lighten") + blend("multiply", alpha = 0.5)) + 206 | scale_color_brewer(palette = "Set1") + 207 | facet_grid(~ order) + 208 | labs( 209 | title = "geom_point(aes(partition = set)) * (blend('lighten') + blend('multiply', alpha = 0.5))", 210 | subtitle = "Two order-independent blends on one layer using the distributive law." 211 | ) + 212 | theme(plot.subtitle = element_text(lineheight = 1.2)) 213 | ``` 214 | 215 | 216 | 217 | ## Blending multiple geometries 218 | 219 | We can also blend geometries together by passing a list of geometries to 220 | `blend()`. These lists can include already-blended geometries: 221 | 222 | ``` r 223 | df |> 224 | ggplot(aes(x, y, color = set, partition = set)) + 225 | list( 226 | geom_point(size = 3, alpha = 0.5) * (blend("lighten") + blend("multiply", alpha = 0.5)), 227 | geom_vline(xintercept = 0, color = "gray75", linewidth = 1.5), 228 | geom_hline(yintercept = 0, color = "gray75", linewidth = 1.5) 229 | ) |> blend("hard.light") + 230 | scale_color_brewer(palette = "Set1") + 231 | facet_grid(~ order) + 232 | labs( 233 | title = "Blending multiple geometries together in a list", 234 | subtitle = "Careful! The point layer blend is incorrect!" 235 | ) 236 | ``` 237 | 238 | 239 | 240 | Whoops!! If you look closely, the blending of the `geom_point()` layers 241 | appears to have changed. Recall that this expression: 242 | 243 | ``` r 244 | geom_point(size = 3, alpha = 0.5) * (blend("lighten") + blend("multiply", alpha = 0.5)) 245 | ``` 246 | 247 | Is equivalent to specifying two separate layers, one with 248 | `blend("lighten")` and the other with 249 | `blend("multiply", alpha = 0.65))`. Thus, when you apply 250 | `|> blend("hard.light")` to the `list()` of layers, it will use a hard 251 | light blend mode to blend these two layers together, when previously 252 | they would be blended using the normal (or `"over"`) blend mode. 253 | 254 | We can gain back the original appearance by blending these two layers 255 | together with `|> blend()` prior to applying the hard light blend: 256 | 257 | ``` r 258 | df |> 259 | ggplot(aes(x, y, color = set, partition = set)) + 260 | list( 261 | geom_point(size = 3, alpha = 0.5) * (blend("lighten") + blend("multiply", alpha = 0.5)) |> blend(), 262 | geom_vline(xintercept = 0, color = "gray75", linewidth = 1.5), 263 | geom_hline(yintercept = 0, color = "gray75", linewidth = 1.5) 264 | ) |> blend("hard.light") + 265 | scale_color_brewer(palette = "Set1") + 266 | facet_grid(~ order) + 267 | labs(title = "Blending multiple geometries together") 268 | ``` 269 | 270 | 271 | 272 | ## Partitioning and blending lineribbons 273 | 274 | Another case where it’s useful to have finer-grained control of blending 275 | within a given geometry is when drawing overlapping uncertainty bands. 276 | Here, we’ll show how to use `blend()` with `stat_lineribbon()` from 277 | [ggdist](https://mjskay.github.io/ggdist/) to create overlapping 278 | gradient ribbons depicting uncertainty. 279 | 280 | We’ll fit a model: 281 | 282 | ``` r 283 | m_mpg = lm(mpg ~ hp * cyl, data = mtcars) 284 | ``` 285 | 286 | And generate some confidence distributions for the mean using 287 | [distributional](https://pkg.mitchelloharawild.com/distributional/): 288 | 289 | ``` r 290 | predictions = unique(mtcars[, c("cyl", "hp")]) 291 | 292 | predictions$mu_hat = with(predict(m_mpg, newdata = predictions, se.fit = TRUE), 293 | distributional::dist_student_t(df = df, mu = fit, sigma = se.fit) 294 | ) 295 | 296 | predictions 297 | ``` 298 | 299 | ## cyl hp mu_hat 300 | ## Mazda RX4 6 110 t(28, 20.28825, 0.7984429) 301 | ## Datsun 710 4 93 t(28, 25.74371, 0.8818612) 302 | ## Hornet Sportabout 8 175 t(28, 15.56144, 0.8638133) 303 | ## Valiant 6 105 t(28, 20.54952, 0.8045354) 304 | ## Duster 360 8 245 t(28, 14.66678, 0.9773475) 305 | ## Merc 240D 4 62 t(28, 28.58736, 1.21846) 306 | ## Merc 230 4 95 t(28, 25.56025, 0.9024699) 307 | ## Merc 280 6 123 t(28, 19.60892, 0.842354) 308 | ## Merc 450SE 8 180 t(28, 15.49754, 0.8332276) 309 | ## Cadillac Fleetwood 8 205 t(28, 15.17801, 0.7674501) 310 | ## Lincoln Continental 8 215 t(28, 15.05021, 0.7866649) 311 | ## Chrysler Imperial 8 230 t(28, 14.85849, 0.8606705) 312 | ## Fiat 128 4 66 t(28, 28.22044, 1.12188) 313 | ## Honda Civic 4 52 t(28, 29.50466, 1.491467) 314 | ## Toyota Corolla 4 65 t(28, 28.31217, 1.145154) 315 | ## Toyota Corona 4 97 t(28, 25.37679, 0.9280143) 316 | ## Dodge Challenger 8 150 t(28, 15.88096, 1.077004) 317 | ## Porsche 914-2 4 91 t(28, 25.92718, 0.8665404) 318 | ## Lotus Europa 4 113 t(28, 23.9091, 1.262843) 319 | ## Ford Pantera L 8 264 t(28, 14.42394, 1.166062) 320 | ## Ferrari Dino 6 175 t(28, 16.89163, 1.550885) 321 | ## Maserati Bora 8 335 t(28, 13.5165, 2.045807) 322 | ## Volvo 142E 4 109 t(28, 24.27603, 1.162526) 323 | 324 | A basic plot based on examples in 325 | `vignette("freq-uncertainty-vis", package = "ggdist")` and 326 | `vignette("lineribbon", package = "ggdist")` may have issues when 327 | lineribbons overlap: 328 | 329 | ``` r 330 | predictions |> 331 | ggplot(aes(x = hp, fill = ordered(cyl), color = ordered(cyl))) + 332 | ggdist::stat_lineribbon( 333 | aes(ydist = mu_hat, fill_ramp = after_stat(.width)), 334 | .width = ppoints(40) 335 | ) + 336 | geom_point(aes(y = mpg), data = mtcars) + 337 | scale_fill_brewer(palette = "Set2") + 338 | scale_color_brewer(palette = "Dark2") + 339 | ggdist::scale_fill_ramp_continuous(range = c(1, 0)) + 340 | labs( 341 | title = "ggdist::stat_lineribbon()", 342 | subtitle = "Overlapping lineribbons obscure each other.", 343 | color = "cyl", fill = "cyl", y = "mpg" 344 | ) 345 | ``` 346 | 347 | 348 | 349 | Notice the overlap of the orange (`cyl = 6`) and purple (`cyl = 8`) 350 | lines. 351 | 352 | If we add a `partition = cyl` aesthetic mapping, we can blend the 353 | geometries for the different levels of `cyl` together with a `blend()` 354 | call around `ggdist::stat_lineribbon()`. 355 | 356 | There are many ways we could add the partition to the plot: 357 | 358 | 1. Add `partition = cyl` to the existing `aes(...)` call. However, this 359 | leaves the partitioning information far from the call to `blend()`, 360 | so the relationship between them is less clear. 361 | 2. Add `aes(partition = cyl)` to the `stat_lineribbon(...)` call. This 362 | is a more localized change (better!), but will raise a warning if 363 | `stat_lineribbon()` itself does not recognized the `partition` 364 | aesthetic. 365 | 3. Add `|> adjust(aes(partition = cyl))` after `stat_lineribbon(...)` 366 | to add the `partition` aesthetic to it (this will bypass the 367 | warning). 368 | 4. Add `|> partition(vars(cyl))` after `stat_lineribbon(...)` to add 369 | the `partition` aesthetic. This is an alias for the `adjust()` 370 | approach that is intended to be clearer. It takes a specification 371 | for a partition that is similar to `facet_wrap()`: either a 372 | one-sided formula or a call to `vars()`. 373 | 374 | Let’s try the fourth approach: 375 | 376 | ``` r 377 | predictions |> 378 | ggplot(aes(x = hp, fill = ordered(cyl), color = ordered(cyl))) + 379 | ggdist::stat_lineribbon( 380 | aes(ydist = mu_hat, fill_ramp = after_stat(.width)), 381 | .width = ppoints(40) 382 | ) |> partition(vars(cyl)) |> blend("multiply") + 383 | geom_point(aes(y = mpg), data = mtcars) + 384 | scale_fill_brewer(palette = "Set2") + 385 | scale_color_brewer(palette = "Dark2") + 386 | ggdist::scale_fill_ramp_continuous(range = c(1, 0)) + 387 | labs( 388 | title = "ggdist::stat_lineribbon() |> partition(vars(cyl)) |> blend('multiply')", 389 | subtitle = "Overlapping lineribbons blend together independent of draw order.", 390 | color = "cyl", fill = "cyl", y = "mpg" 391 | ) 392 | ``` 393 | 394 | 395 | 396 | Now the overlapping ribbons are blended together. 397 | 398 | ## Highlighting geoms using `copy_under()` 399 | 400 | A common visualization technique to make a layer more salient 401 | (especially in the presence of many other competing layers) is to add a 402 | small outline around it. For some geometries (like `geom_point()`) this 403 | is easy; but for others (like `geom_line()`), there’s no easy way to do 404 | this without manually copying the layer. 405 | 406 | The *ggblend* layer algebra makes this straightforward using the 407 | `adjust()` operation combined with operator addition and multiplication. 408 | For example, given a layer like: 409 | 410 | ``` r 411 | geom_line(linewidth = 1) 412 | ``` 413 | 414 | To add a white outline, you might want something like: 415 | 416 | ``` r 417 | geom_line(color = "white", linewidth = 2.5) + geom_line(linewidth = 1) 418 | ``` 419 | 420 | However, we’d rather not have to write the `geom_line()` specification 421 | twice If we factor out the differences between the first and second 422 | layer, we can use the `adjust()` operation (which lets you change the 423 | aesthetics and parameters of a layer) along with the distributive law to 424 | factor out `geom_line(linewidth = 1)` and write the above specification 425 | as: 426 | 427 | ``` r 428 | geom_line(linewidth = 1) * (adjust(color = "white", linewidth = 2.5) + 1) 429 | ``` 430 | 431 | The `copy_under(...)` operation, which is a synonym for 432 | `adjust(...) + 1`, also implements this pattern: 433 | 434 | ``` r 435 | geom_line(linewidth = 1) * copy_under(color = "white", linewidth = 2.5) 436 | ``` 437 | 438 | Here’s an example highlighting the fit lines from our previous 439 | lineribbon example: 440 | 441 | ``` r 442 | predictions |> 443 | ggplot(aes(x = hp, fill = ordered(cyl), color = ordered(cyl))) + 444 | ggdist::stat_ribbon( 445 | aes(ydist = mu_hat, fill_ramp = after_stat(.width)), 446 | .width = ppoints(40) 447 | ) |> partition(vars(cyl)) |> blend("multiply") + 448 | geom_line(aes(y = median(mu_hat)), linewidth = 1) |> copy_under(color = "white", linewidth = 2.5) + 449 | geom_point(aes(y = mpg), data = mtcars) + 450 | scale_fill_brewer(palette = "Set2") + 451 | scale_color_brewer(palette = "Dark2") + 452 | ggdist::scale_fill_ramp_continuous(range = c(1, 0)) + 453 | labs( 454 | title = "geom_line() |> copy_under(color = 'white', linewidth = 2.5)", 455 | subtitle = "Highlights the line layer without manually copying its specification.", 456 | color = "cyl", fill = "cyl", y = "mpg" 457 | ) 458 | ``` 459 | 460 | 461 | 462 | Note that the implementation of `copy_under(...)` is simply a synonym 463 | for `adjust(...) + 1`; we can see this if we look at `copy_under()` 464 | itself: 465 | 466 | ``` r 467 | copy_under() 468 | ``` 469 | 470 | ## : (adjust() + 1) 471 | 472 | In fact, not that it is particularly useful, but addition and 473 | multiplication of layer operations is expanded appropriately: 474 | 475 | ``` r 476 | (adjust() + 3) * 2 477 | ``` 478 | 479 | ## : (adjust() + 1 + 1 + 1 + adjust() + 1 + 1 + 1) 480 | 481 | I hesitate to imagine what that feature might be useful for… 482 | 483 | ## Compatibility with other packages 484 | 485 | In theory *ggblend* should be compatible with other packages, though in 486 | more complex cases (blending lists of geoms or using the `partition` 487 | aesthetic) it is possible it may fail, as these features are a bit more 488 | hackish. I have done some testing with a few other layer-manipulating 489 | packages—including [gganimate](https://gganimate.com/), 490 | [ggnewscale](https://eliocamp.github.io/ggnewscale/), and 491 | [relayer](https://github.com/clauswilke/relayer)—and they appear to be 492 | compatible. 493 | 494 | As a hard test, here is all three features applied to a modified version 495 | of the Gapminder example used in the [gganimate 496 | documentation](https://gganimate.com/): 497 | 498 | ``` r 499 | library(gganimate) 500 | library(gapminder) 501 | 502 | p = gapminder |> 503 | ggplot(aes(gdpPercap, lifeExp, size = pop, color = continent)) + 504 | list( 505 | geom_point(show.legend = c(size = FALSE)) |> partition(vars(continent)) |> blend("multiply"), 506 | geom_hline(yintercept = 70, linewidth = 1.5, color = "gray75") 507 | ) |> blend("hard.light") + 508 | scale_color_manual( 509 | # same as colorspace::lighten(continent_colors, 0.35) 510 | values = c( 511 | Africa = "#BE7658", Americas = "#E95866", Asia = "#7C5C86", 512 | Europe = "#659C5D", Oceania = "#7477CA" 513 | ), 514 | guide = guide_legend(override.aes = list(size = 4)) 515 | ) + 516 | scale_size(range = c(2, 12)) + 517 | scale_x_log10(labels = scales::label_dollar(scale_cut = scales::cut_short_scale())) + 518 | scale_y_continuous(breaks = seq(20, 80, by = 10)) + 519 | labs( 520 | title = 'Gapminder with gganimate and ggblend', 521 | subtitle = 'Year: {frame_time}', 522 | x = 'GDP per capita', 523 | y = 'Life expectancy' 524 | ) + 525 | transition_time(year) + 526 | ease_aes('linear') 527 | 528 | animate(p, type = "cairo", width = 600, height = 400, res = 100) 529 | ``` 530 | 531 | ![](man/figures/README-gapminder-1.gif) 532 | -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: https://mjskay.github.io/ggblend/ 2 | 3 | authors: 4 | Matthew Kay: 5 | href: http://www.mjskay.com 6 | 7 | template: 8 | bootstrap: 5 9 | bslib: 10 | line-height-base: 1.7 11 | primary: "#4575B4" 12 | base_font: 13 | google: 14 | family: "Source Sans Pro" 15 | wght: [400, 600, 700] 16 | ital: [0, 1] 17 | code_font: 18 | google: 19 | family: "Source Code Pro" 20 | wght: [400, 700] 21 | ital: [0, 1] 22 | 23 | figures: 24 | dev: svg 25 | fig.ext: svg 26 | 27 | reference: 28 | - title: Package overview 29 | desc: Overview of ggblend. 30 | contents: ggblend-package 31 | - title: Layer operations 32 | desc: | 33 | Operations that can be applied to transform ggplot2 layers and layer-like objects 34 | contents: 35 | - nop 36 | - adjust 37 | - copy 38 | - partition 39 | - blend 40 | - affine_transform 41 | - operation 42 | - title: Combinations of layer operations 43 | desc: | 44 | Combinations of layer operations created through addition (`+`), multiplication (`*`), and function composition (`|>`). 45 | contents: 46 | - operation_sum 47 | - operation_product 48 | - operation_composition 49 | - title: Layers and layer-like objects 50 | desc: | 51 | Objects that represent ggplot2 layers and layer-like objects 52 | contents: 53 | - layer-like 54 | - layer_list 55 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | This is a new submission. There is 1 NOTE for potentially misspelled words in DESCRIPTION (see below). These words are all spelled correctly. 2 | 3 | ## R CMD check results 4 | 5 | 0 errors | 0 warnings | 1 note 6 | 7 | New submission 8 | 9 | Possibly misspelled words in DESCRIPTION: 10 | Compositing (2:21) 11 | Kindlmann (15:9) 12 | Scheidegger (15:23) 13 | affine (10:55) 14 | compositing (8:5) 15 | -------------------------------------------------------------------------------- /figures-source/ggplot2-ext-gallery.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggblend/1c08d8767a113ddc86634d41b353b15d71e34126/figures-source/ggplot2-ext-gallery.pdf -------------------------------------------------------------------------------- /figures-source/ggplot2-ext-gallery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggblend/1c08d8767a113ddc86634d41b353b15d71e34126/figures-source/ggplot2-ext-gallery.png -------------------------------------------------------------------------------- /figures-source/logo.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggblend/1c08d8767a113ddc86634d41b353b15d71e34126/figures-source/logo.pdf -------------------------------------------------------------------------------- /ggblend.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 | -------------------------------------------------------------------------------- /inst/CITATION: -------------------------------------------------------------------------------- 1 | year <- sub("-.*", "", meta$Date) 2 | note <- sprintf("R package version %s", meta$Version) 3 | 4 | bibentry( 5 | bibtype = "Manual", 6 | title = paste("{ggblend}: Blending and Compositing Algebra for {ggplot2}"), 7 | author = c(person("Matthew", "Kay")), 8 | year = year, 9 | note = note, 10 | url = "https://mjskay.github.io/ggblend/", 11 | doi = "10.5281/zenodo.7963886" 12 | ) 13 | -------------------------------------------------------------------------------- /inst/WORDLIST: -------------------------------------------------------------------------------- 1 | Affine 2 | CMD 3 | Codecov 4 | Gapminder 5 | Kindlmann 6 | Lifecycle 7 | Murrell 8 | Params 9 | Scheidegger 10 | TVCG 11 | affine 12 | composable 13 | doi 14 | geoms 15 | gganimate 16 | ggdist 17 | ggnewscale 18 | ggplot 19 | lineribbon 20 | lineribbons 21 | params 22 | quosures 23 | relayer 24 | -------------------------------------------------------------------------------- /man-roxygen/operation.R: -------------------------------------------------------------------------------- 1 | #' @return 2 | #' A [layer-like] object (if `object` is [layer-like]) or an [operation] (if not). 3 | #' 4 | #' @family layer operations 5 | #' @seealso [operation] for a description of layer operations. 6 | -------------------------------------------------------------------------------- /man/adjust.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/class-operation-adjust.R, R/operation-adjust.R 3 | \docType{class} 4 | \name{adjust} 5 | \alias{adjust} 6 | \alias{adjust-class} 7 | \title{Adjust layer params and aesthetics (Layer operation)} 8 | \usage{ 9 | adjust(object, mapping = aes(), ...) 10 | } 11 | \arguments{ 12 | \item{object}{One of: 13 | \itemize{ 14 | \item A \link{layer-like} object: applies this operation to the layer. 15 | \item A missing argument: creates an \link{operation} 16 | \item Anything else: creates an \link{operation}, passing \code{object} along to the 17 | \code{mapping} argument 18 | }} 19 | 20 | \item{mapping}{An aesthetic created using \code{aes()}. Mappings provided here 21 | will overwrite mappings in \code{\link[ggplot2:layer]{ggplot2::layer()}}s when this \link{operation} is applied to 22 | them.} 23 | 24 | \item{...}{\code{\link[ggplot2:layer]{ggplot2::layer()}} parameters, such as would be passed to a \code{geom_...()} 25 | or \code{stat_...()} call. Params provided here will overwrite params in layers when 26 | this \link{operation} is applied to them.} 27 | } 28 | \value{ 29 | A \link{layer-like} object (if \code{object} is \link{layer-like}) or an \link{operation} (if not). 30 | } 31 | \description{ 32 | A layer \link{operation} for adjusting the params and aesthetic mappings of 33 | a \link{layer-like} object. 34 | } 35 | \examples{ 36 | 37 | library(ggplot2) 38 | 39 | # Here we use adjust() with nop() ( + 1) to create a copy of 40 | # the stat_smooth layer, putting a white outline around it. 41 | set.seed(1234) 42 | k = 1000 43 | data.frame( 44 | x = seq(1, 10, length.out = k), 45 | y = rnorm(k, seq(1, 2, length.out = k) + c(0, 0.5)), 46 | g = c("a", "b") 47 | ) |> 48 | ggplot(aes(x, y, color = g)) + 49 | geom_point() + 50 | stat_smooth(method = lm, formula = y ~ x, linewidth = 1.5, se = FALSE) * 51 | (adjust(aes(group = g), color = "white", linewidth = 4) + 1) + 52 | scale_color_brewer(palette = "Dark2") 53 | 54 | # (note this could also be done with copy_under()) 55 | 56 | } 57 | \seealso{ 58 | \link{operation} for a description of layer operations. 59 | 60 | Other layer operations: 61 | \code{\link{affine_transform}}, 62 | \code{\link{blend}}, 63 | \code{\link{copy}}, 64 | \code{\link{nop}}, 65 | \code{\link{partition}()} 66 | } 67 | \concept{layer operations} 68 | -------------------------------------------------------------------------------- /man/affine_transform.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/class-operation-affine-transform.R, 3 | % R/operation-affine-transform.R 4 | \docType{class} 5 | \name{affine_transform} 6 | \alias{affine_transform} 7 | \alias{affine_transform-class} 8 | \title{Translate, scale, and rotate ggplot2 layers (Layer operation)} 9 | \usage{ 10 | affine_transform(object, x = 0, y = 0, width = 1, height = 1, angle = 0) 11 | } 12 | \arguments{ 13 | \item{object}{One of: 14 | \itemize{ 15 | \item A \link{layer-like} object: applies this operation to the layer. 16 | \item A missing argument: creates an \link{operation} 17 | \item A \code{numeric()} or \code{unit()} giving the x-axis translation, 18 | which takes the place of the \code{x} argument. 19 | }} 20 | 21 | \item{x}{A \code{numeric()} or \code{unit()} giving the x translation to apply.} 22 | 23 | \item{y}{A \code{numeric()} or \code{unit()} giving the y translation to apply.} 24 | 25 | \item{width}{A \code{numeric()} or \code{unit()} giving the width.} 26 | 27 | \item{height}{A \code{numeric()} or \code{unit()} giving the height.} 28 | 29 | \item{angle}{A \code{numeric()} giving the angle to rotate, in degrees.} 30 | } 31 | \value{ 32 | A \link{layer-like} object (if \code{object} is \link{layer-like}) or an \link{operation} (if not). 33 | } 34 | \description{ 35 | Transform objects within a single layer (geom) or across multiple layers (geoms) 36 | using affine transformations, like translation, scale, and rotation. Uses 37 | the built-in compositing support in graphical devices added in R 4.2. 38 | } 39 | \details{ 40 | Applies an affine transformation (translation, scaling, rotation) to a layer. 41 | 42 | \strong{Note:} due to limitations in the implementation of scaling and rotation, 43 | currently these operations can only be performed relative to the center of 44 | the plot. In future versions, the translation and rotation origin may be 45 | configurable. 46 | } 47 | \section{Supported devices}{ 48 | 49 | Transformation is not currently supported by all graphics devices. As of this writing, 50 | at least \code{png(type = "cairo")}, \code{svg()}, and \code{cairo_pdf()} are known to support 51 | blending. 52 | 53 | \code{affine_transform()} attempts to auto-detect support for affine transformation using \code{dev.capabilities()}. 54 | You may receive a warning when using \code{affine_transform()} if it appears transformation is not 55 | supported by the current graphics device. This warning \strong{either} means (1) 56 | your graphics device does not support transformation (in which case you should 57 | switch to one that does) or (2) your graphics device 58 | supports transformation but incorrectly reports that it does not. Unfortunately, 59 | not all graphics devices that support transformation appear to correctly \emph{report} 60 | that they support transformation, so even if auto-detection fails, \code{blend()} will 61 | still attempt to apply the transformation, just in case. 62 | 63 | If the warning is issued and the output is still correctly transformed, this is 64 | likely a bug in the graphics device. You can report the bug to the authors of 65 | the graphics device if you wish; in the mean time, you can use 66 | \code{options(ggblend.check_affine_transform = FALSE)} to disable the check. 67 | } 68 | 69 | \examples{ 70 | \dontshow{old_options = options(ggblend.check_affine_transform = FALSE)} 71 | library(ggplot2) 72 | 73 | # a simple dataset: 74 | set.seed(1234) 75 | data.frame(x = rnorm(100), y = rnorm(100)) |> 76 | ggplot(aes(x, y)) + 77 | geom_point() + 78 | xlim(-5, 5) 79 | 80 | # we could scale and translate copies of the point cloud 81 | # (though I'm not sure why...) 82 | data.frame(x = rnorm(100), y = rnorm(100)) |> 83 | ggplot(aes(x, y)) + 84 | geom_point() * ( 85 | affine_transform(x = -unit(100, "pt"), width = 0.5) |> adjust(color = "red") + 86 | affine_transform(width = 0.5) + 87 | affine_transform(x = unit(100, "pt"), width = 0.5) |> adjust(color = "blue") 88 | ) + 89 | xlim(-5, 5) 90 | \dontshow{options(old_options)} 91 | } 92 | \references{ 93 | Murrell, Paul (2021): 94 | \href{https://www.stat.auckland.ac.nz/~paul/Reports/GraphicsEngine/groups/groups.html}{Groups, Compositing Operators, and Affine Transformations in R Graphics}. 95 | The University of Auckland. Report. 96 | \doi{10.17608/k6.auckland.17009120.v1}. 97 | } 98 | \seealso{ 99 | \link{operation} for a description of layer operations. 100 | 101 | Other layer operations: 102 | \code{\link{adjust}}, 103 | \code{\link{blend}}, 104 | \code{\link{copy}}, 105 | \code{\link{nop}}, 106 | \code{\link{partition}()} 107 | } 108 | \concept{layer operations} 109 | -------------------------------------------------------------------------------- /man/blend.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/class-operation-blend.R, R/operation-blend.R 3 | \docType{class} 4 | \name{blend} 5 | \alias{blend} 6 | \alias{blend-class} 7 | \title{Blend ggplot2 layers (Layer operation)} 8 | \usage{ 9 | blend(object, blend = "over", alpha = 1) 10 | } 11 | \arguments{ 12 | \item{object}{One of: 13 | \itemize{ 14 | \item A \link{layer-like} object: applies this operation to the layer. 15 | \item A missing argument: creates an \link{operation} 16 | \item A string (character vector of length 1) giving the name of a blend, 17 | which takes the place of the \code{blend} argument. 18 | }} 19 | 20 | \item{blend}{The blend mode to use. The default mode, \code{"over"}, corresponds to 21 | the "usual" blend mode of drawing objects on top of each other. 22 | The list of supported blend modes depends on your graphical device 23 | (see Murrell 2021), and are listed in \code{dev.capabilities()$compositing}. 24 | Blend modes can include: \code{"clear"}, 25 | \code{"source"}, \code{"over"}, \code{"in"}, \code{"out"}, \code{"atop"}, \code{"dest"}, \code{"dest.over"}, 26 | \code{"dest.in"}, \code{"dest.out"}, \code{"dest.atop"}, \code{"xor"}, \code{"add"}, \code{"saturate"}, 27 | \code{"multiply"}, \code{"screen"}, \code{"overlay"}, \code{"darken"}, \code{"lighten"}, 28 | \code{"color.dodge"}, \code{"color.burn"}, \code{"hard.light"}, \code{"soft.light"}, 29 | \code{"difference"}, and \code{"exclusion"} 30 | 31 | Blend modes like \code{"multiply"}, \code{"darken"}, and \code{"lighten"} are particularly useful as they 32 | are \emph{commutative}: the result is the same whichever order they are applied in. 33 | 34 | A warning is issued if the current graphics device does not appear to support 35 | the requested blend mode. In some cases this warning may be spurious, so 36 | it can be disabled by setting \code{options(ggblend.check_blend = FALSE)}.} 37 | 38 | \item{alpha}{A numeric between \code{0} and \code{1} (inclusive). The opacity of a 39 | transparency mask applied to objects prior to blending.} 40 | } 41 | \value{ 42 | A \link{layer-like} object (if \code{object} is \link{layer-like}) or an \link{operation} (if not). 43 | } 44 | \description{ 45 | Blend objects within a single layer (geom) or across multiple layers (geoms) 46 | using graphical blending modes, such as \code{"multiply"}, \code{"overlay"}, etc. Uses 47 | the built-in compositing support in graphical devices added in R 4.2. 48 | } 49 | \details{ 50 | If \code{object} is a single layer / geometry and the \code{partition} aesthetic \emph{is not} set, every 51 | graphical object (\code{\link[=grob]{grob()}}) output by the geometry will be blended together 52 | using the \code{blend} blend mode. If \code{alpha != 1}, a transparency mask with the 53 | provided alpha level will be applied to each grob before blending. 54 | 55 | If \code{object} is a single layer / geometry and the \code{partition} aesthetic \emph{is} set, 56 | the geometry will be rendered for each subset of the data defined by the 57 | \code{partition} aesthetic, a transparency mask with the provided \code{alpha} level 58 | will be applied to each resulting group as a whole (if \code{alpha != 1}), then these groups 59 | will be blended together using the \code{blend} blend mode. 60 | 61 | If \code{object} is a list of layers / geometries, those layers will be rendered 62 | separately, a transparency mask with the provided \code{alpha} level 63 | will be applied to each layer as a whole (if \code{alpha != 1}), then these layers 64 | will be blended together using the \code{blend} blend mode. 65 | 66 | If a \code{blend()} is multiplied by a list of layers using \code{*}, it acts on each 67 | layer individually (as if each layer were passed to \code{blend()}). 68 | } 69 | \section{Supported devices}{ 70 | 71 | Blending is not currently supported by all graphics devices. As of this writing, 72 | at least \code{png(type = "cairo")}, \code{svg()}, and \code{cairo_pdf()} are known to support 73 | blending. 74 | 75 | \code{blend()} attempts to auto-detect support for blending using \code{dev.capabilities()}. 76 | You may receive a warning when using \code{blend()} if it appears blending is not 77 | supported by the current graphics device. This warning \strong{either} means (1) 78 | your graphics device does not support blending (in which case you should 79 | switch to one that does) or (2) your graphics device 80 | supports blending but incorrectly reports that it does not. Unfortunately, 81 | not all graphics devices that support blending appear to correctly \emph{report} 82 | that they support blending, so even if auto-detection fails, \code{blend()} will 83 | still attempt to apply the blend, just in case. 84 | 85 | If the warning is issued and the output is still correctly blended, this is 86 | likely a bug in the graphics device. You can report the bug to the authors of 87 | the graphics device if you wish; in the mean time, you can use 88 | \code{options(ggblend.check_blend = FALSE)} to disable the check. 89 | } 90 | 91 | \examples{ 92 | \dontshow{old_options = options(ggblend.check_blend = FALSE)} 93 | library(ggplot2) 94 | 95 | # create two versions of a dataset, where draw order can affect output 96 | set.seed(1234) 97 | df_a = data.frame(x = rnorm(500, 0), y = rnorm(500, 1), set = "a") 98 | df_b = data.frame(x = rnorm(500, 1), y = rnorm(500, 2), set = "b") 99 | df_ab = rbind(df_a, df_b) |> 100 | transform(order = "draw a then b") 101 | df_ba = rbind(df_b, df_a) |> 102 | transform(order = "draw b then a") 103 | df = rbind(df_ab, df_ba) 104 | 105 | # Using the "darken" blend mode, draw order does not matter: 106 | df |> 107 | ggplot(aes(x, y, color = set)) + 108 | geom_point(size = 3) |> blend("darken") + 109 | scale_color_brewer(palette = "Set2") + 110 | facet_grid(~ order) 111 | 112 | # Using the "multiply" blend mode, we can see density within groups: 113 | df |> 114 | ggplot(aes(x, y, color = set)) + 115 | geom_point(size = 3) |> blend("multiply") + 116 | scale_color_brewer(palette = "Set2") + 117 | facet_grid(~ order) 118 | 119 | # blend() on a single geom by default blends all grobs in that geom together 120 | # using the requested blend mode. If we wish to blend within specific data 121 | # subsets using normal blending ("over") but between subsets using the 122 | # requested blend mode, we can set the partition aesthetic. This will 123 | # make "multiply" behave more like "darken": 124 | df |> 125 | ggplot(aes(x, y, color = set, partition = set)) + 126 | geom_point(size = 3) |> blend("multiply") + 127 | scale_color_brewer(palette = "Set2") + 128 | facet_grid(~ order) 129 | 130 | # We can also blend lists of geoms together; these geoms are rendered using 131 | # normal ("over") blending (unless a blend() call is applied to a specific 132 | # sub-layer, as in the first layer below) and then blended together using 133 | # the requested blend mode. 134 | df |> 135 | ggplot(aes(x, y, color = set)) + 136 | list( 137 | geom_point(size = 3) |> blend("darken"), 138 | geom_vline(xintercept = 0, color = "gray75", linewidth = 1.5), 139 | geom_hline(yintercept = 0, color = "gray75", linewidth = 1.5) 140 | ) |> blend("hard.light") + 141 | scale_color_brewer(palette = "Set2") + 142 | facet_grid(~ order) 143 | \dontshow{options(old_options)} 144 | } 145 | \references{ 146 | Murrell, Paul (2021): 147 | \href{https://www.stat.auckland.ac.nz/~paul/Reports/GraphicsEngine/groups/groups.html}{Groups, Compositing Operators, and Affine Transformations in R Graphics}. 148 | The University of Auckland. Report. 149 | \doi{10.17608/k6.auckland.17009120.v1}. 150 | } 151 | \seealso{ 152 | \link{operation} for a description of layer operations. 153 | 154 | Other layer operations: 155 | \code{\link{adjust}}, 156 | \code{\link{affine_transform}}, 157 | \code{\link{copy}}, 158 | \code{\link{nop}}, 159 | \code{\link{partition}()} 160 | } 161 | \concept{layer operations} 162 | -------------------------------------------------------------------------------- /man/copy.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/operation-copy.R 3 | \name{copy} 4 | \alias{copy} 5 | \alias{copy_over} 6 | \alias{copy_under} 7 | \title{Copy layers then adjust params and aesthetics (Layer operation)} 8 | \usage{ 9 | copy_over(object, mapping = aes(), ...) 10 | 11 | copy_under(object, mapping = aes(), ...) 12 | } 13 | \arguments{ 14 | \item{object}{One of: 15 | \itemize{ 16 | \item A \link{layer-like} object: applies this operation to the layer. 17 | \item A missing argument: creates an \link{operation} 18 | \item Anything else: creates an \link{operation}, passing \code{object} along to the 19 | \code{mapping} argument 20 | }} 21 | 22 | \item{mapping}{An aesthetic created using \code{aes()}. Mappings provided here 23 | will overwrite mappings in \code{\link[ggplot2:layer]{ggplot2::layer()}}s when this \link{operation} is applied to 24 | them.} 25 | 26 | \item{...}{\code{\link[ggplot2:layer]{ggplot2::layer()}} parameters, such as would be passed to a \code{geom_...()} 27 | or \code{stat_...()} call. Params provided here will overwrite params in layers when 28 | this \link{operation} is applied to them.} 29 | } 30 | \value{ 31 | A \link{layer-like} object (if \code{object} is \link{layer-like}) or an \link{operation} (if not). 32 | } 33 | \description{ 34 | A layer \link{operation} for copying and then adjusting the params and aesthetic 35 | mappings of a \link{layer-like} object. 36 | } 37 | \details{ 38 | These are shortcuts for duplicating a layer and then applying \code{\link[=adjust]{adjust()}}. 39 | Specifically: 40 | \itemize{ 41 | \item \code{copy_over(...)} is equivalent to \code{1 + adjust(...)} 42 | \item \code{copy_under(...)} is equivalent to \code{adjust(...) + 1} 43 | } 44 | } 45 | \examples{ 46 | library(ggplot2) 47 | 48 | # here we use copy_under() to create a copy of 49 | # the stat_smooth layer, putting a white outline around it. 50 | set.seed(1234) 51 | k = 1000 52 | data.frame( 53 | x = seq(1, 10, length.out = k), 54 | y = rnorm(k, seq(1, 2, length.out = k) + c(0, 0.5)), 55 | g = c("a", "b") 56 | ) |> 57 | ggplot(aes(x, y, color = g)) + 58 | geom_point() + 59 | stat_smooth(method = lm, formula = y ~ x, linewidth = 1.5, se = FALSE) * 60 | copy_under(aes(group = g), color = "white", linewidth = 4) + 61 | scale_color_brewer(palette = "Dark2") 62 | 63 | } 64 | \seealso{ 65 | \link{operation} for a description of layer operations. 66 | 67 | Other layer operations: 68 | \code{\link{adjust}}, 69 | \code{\link{affine_transform}}, 70 | \code{\link{blend}}, 71 | \code{\link{nop}}, 72 | \code{\link{partition}()} 73 | } 74 | \concept{layer operations} 75 | -------------------------------------------------------------------------------- /man/figures/README-gapminder-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggblend/1c08d8767a113ddc86634d41b353b15d71e34126/man/figures/README-gapminder-1.gif -------------------------------------------------------------------------------- /man/figures/README-lineribbon_blend-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggblend/1c08d8767a113ddc86634d41b353b15d71e34126/man/figures/README-lineribbon_blend-1.png -------------------------------------------------------------------------------- /man/figures/README-lineribbon_blend_highlight-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggblend/1c08d8767a113ddc86634d41b353b15d71e34126/man/figures/README-lineribbon_blend_highlight-1.png -------------------------------------------------------------------------------- /man/figures/README-lineribbon_noblend-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggblend/1c08d8767a113ddc86634d41b353b15d71e34126/man/figures/README-lineribbon_noblend-1.png -------------------------------------------------------------------------------- /man/figures/README-scatter_blend-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggblend/1c08d8767a113ddc86634d41b353b15d71e34126/man/figures/README-scatter_blend-1.png -------------------------------------------------------------------------------- /man/figures/README-scatter_blend_geom-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggblend/1c08d8767a113ddc86634d41b353b15d71e34126/man/figures/README-scatter_blend_geom-1.png -------------------------------------------------------------------------------- /man/figures/README-scatter_blend_geom_incorrect-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggblend/1c08d8767a113ddc86634d41b353b15d71e34126/man/figures/README-scatter_blend_geom_incorrect-1.png -------------------------------------------------------------------------------- /man/figures/README-scatter_lighten_multiply-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggblend/1c08d8767a113ddc86634d41b353b15d71e34126/man/figures/README-scatter_lighten_multiply-1.png -------------------------------------------------------------------------------- /man/figures/README-scatter_lighten_multiply_stacked-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggblend/1c08d8767a113ddc86634d41b353b15d71e34126/man/figures/README-scatter_lighten_multiply_stacked-1.png -------------------------------------------------------------------------------- /man/figures/README-scatter_noblend-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggblend/1c08d8767a113ddc86634d41b353b15d71e34126/man/figures/README-scatter_noblend-1.png -------------------------------------------------------------------------------- /man/figures/README-scatter_partition_blend-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggblend/1c08d8767a113ddc86634d41b353b15d71e34126/man/figures/README-scatter_partition_blend-1.png -------------------------------------------------------------------------------- /man/figures/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 158 | 159 | -------------------------------------------------------------------------------- /man/ggblend-package.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/ggblend-package.R 3 | \docType{package} 4 | \name{ggblend-package} 5 | \alias{ggblend-package} 6 | \alias{ggblend} 7 | \title{Blending and compositing for ggplot2} 8 | \description{ 9 | \pkg{ggblend} is an R package that adds support for R 4.2 blend modes 10 | (e.g. \code{"multiply"}, \code{"overlay"}, etc) to \pkg{ggplot2}. 11 | } 12 | \details{ 13 | The primary support for blending is provided by the \code{blend()} function, 14 | which can be used to augment \code{\link[=ggplot]{ggplot()}} layers/geoms or lists of 15 | layers/geoms in a \code{\link[=ggplot]{ggplot()}} specification. 16 | 17 | For example, one can replace something like this: 18 | 19 | \if{html}{\out{
}}\preformatted{df |> 20 | ggplot(aes(x, y)) + 21 | geom_X(...) + 22 | geom_Y(...) + 23 | geom_Z(...) 24 | }\if{html}{\out{
}} 25 | 26 | With something like this: 27 | 28 | \if{html}{\out{
}}\preformatted{df |> 29 | ggplot(aes(x, y)) + 30 | geom_X(...) + 31 | geom_Y(...) |> blend("multiply") + 32 | geom_Z(...) 33 | }\if{html}{\out{
}} 34 | 35 | In order to apply a "multiply" blend to the layer with \code{geom_Y(...)}. 36 | } 37 | \section{Package options}{ 38 | 39 | 40 | The following global options can be set using \code{\link[=options]{options()}} to modify the 41 | behavior of \pkg{ggblend}: 42 | \itemize{ 43 | \item \code{"ggblend.check_blend"}: If \code{TRUE} (default), \code{\link[=blend]{blend()}} will warn if 44 | you attempt to use a blend mode not supported by the current graphics 45 | device, as reported by \code{dev.capabilities()$compositing}. Since this check 46 | can be unreliable on some devices (they will report not support a blend 47 | mode that they do support), you can disable this warning by setting this 48 | option to \code{FALSE}. 49 | \item \code{"ggblend.check_affine_transform"}: If \code{TRUE} (default), \code{\link[=affine_transform]{affine_transform()}} will warn if 50 | you attempt to use a blend mode not supported by the current graphics 51 | device, as reported by \code{dev.capabilities()$transformation}. Since this check 52 | can be unreliable on some devices (they will report not support a blend 53 | mode that they do support), you can disable this warning by setting this 54 | option to \code{FALSE}. 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /man/layer-like.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/layer-.R 3 | \name{layer-like} 4 | \alias{layer-like} 5 | \alias{layer} 6 | \alias{is_layer_like} 7 | \alias{as_layer_like} 8 | \alias{as_layer_like.default} 9 | \alias{as_layer_like.LayerInstance} 10 | \alias{as_layer_like.list} 11 | \alias{as_layer_like.layer_list} 12 | \title{ggplot2 layer-like objects} 13 | \usage{ 14 | is_layer_like(x) 15 | 16 | as_layer_like(x) 17 | 18 | \method{as_layer_like}{default}(x) 19 | 20 | \method{as_layer_like}{LayerInstance}(x) 21 | 22 | \method{as_layer_like}{list}(x) 23 | 24 | \method{as_layer_like}{layer_list}(x) 25 | } 26 | \arguments{ 27 | \item{x}{A \link{layer-like} object. See \emph{Details}.} 28 | } 29 | \value{ 30 | For \code{is_layer_like()}, a \code{logical}: \code{TRUE} if \code{x} is layer-like, \code{FALSE} otherwise. 31 | 32 | For \code{as_layer_like()}, a \code{"LayerInstance"} or a \code{\link[=layer_list]{layer_list()}}. 33 | } 34 | \description{ 35 | For technical reasons related to how \pkg{ggplot2} implements layers, there 36 | is no single class from which all valid \pkg{ggplot2} layers and lists of 37 | layers inherit. Thus, \pkg{ggblend} \link{operation}s supports a variety of "layer-like" 38 | objects, documented here (see \emph{Details}). 39 | } 40 | \details{ 41 | \pkg{ggblend} \link{operation}s can be applied to several \code{\link[ggplot2:layer]{ggplot2::layer()}}-like objects, 42 | including: 43 | \itemize{ 44 | \item objects of class \code{"LayerInstance"}; e.g. \code{stat}s and \code{geom}s. 45 | \item \code{\link[=list]{list()}}s of layer-like objects. 46 | \item \code{\link[=layer_list]{layer_list()}}s, which are a more type-safe version of \code{\link[=list]{list()}}s of 47 | layer-like objects. 48 | } 49 | 50 | Anywhere in \pkg{ggblend} where a function parameter is documented as being 51 | \link{layer-like}, it can be any of the above object types. 52 | } 53 | \section{Functions}{ 54 | \itemize{ 55 | \item \code{is_layer_like()}: checks if an object is layer-like according to \pkg{ggblend}. 56 | 57 | \item \code{as_layer_like()}: validates that an object is layer-like and converts 58 | it to a \code{"LayerInstance"} or \code{\link[=layer_list]{layer_list()}}. 59 | 60 | }} 61 | \examples{ 62 | library(ggplot2) 63 | 64 | is_layer_like(geom_line()) 65 | is_layer_like(list(geom_line())) 66 | is_layer_like(list(geom_line(), scale_x_continuous())) 67 | is_layer_like(list(geom_line(), "abc")) 68 | } 69 | -------------------------------------------------------------------------------- /man/layer_list.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/class-layer-list.R, R/layer-list.R 3 | \docType{class} 4 | \name{layer_list} 5 | \alias{layer_list} 6 | \alias{layer_list-class} 7 | \alias{as_layer_list} 8 | \alias{as_layer_list.layer_list} 9 | \alias{as_layer_list.list} 10 | \alias{as_layer_list.LayerInstance} 11 | \alias{+,layer_list,layer_list-method} 12 | \alias{show,layer_list-method} 13 | \title{Lists of layer-like objects} 14 | \usage{ 15 | layer_list(...) 16 | 17 | as_layer_list(x) 18 | 19 | \method{as_layer_list}{layer_list}(x) 20 | 21 | \method{as_layer_list}{list}(x) 22 | 23 | \method{as_layer_list}{LayerInstance}(x) 24 | 25 | \S4method{+}{layer_list,layer_list}(e1, e2) 26 | 27 | \S4method{show}{layer_list}(object) 28 | } 29 | \arguments{ 30 | \item{x, ...}{\link{layer-like} objects} 31 | 32 | \item{object, e1, e2}{\code{\link[=layer_list]{layer_list()}}s} 33 | } 34 | \value{ 35 | An object of class \code{"layer_list"}. 36 | } 37 | \description{ 38 | A list of \link{layer-like} objects, which can be used in layer \link{operation}s 39 | (through function application or multiplication) or added to a \code{\link[=ggplot2]{ggplot2()}} 40 | object. 41 | } 42 | \details{ 43 | For the most part, users of \pkg{ggblend} need not worry about this class. 44 | It is used internally to simplify multiple dispatch on binary operators, as 45 | the alternative (\code{\link[=list]{list()}}s of \code{\link[ggplot2:layer]{ggplot2::layer()}}s) is more cumbersome. 46 | \pkg{ggblend} converts input lists to this format as needed. 47 | } 48 | \examples{ 49 | library(ggplot2) 50 | 51 | # layer_list()s act just like list()s of layer()s in that they can 52 | # be added to ggplot() objects 53 | data.frame(x = 1:10) |> 54 | ggplot(aes(x, x)) + 55 | layer_list( 56 | geom_line(), 57 | geom_point() 58 | ) 59 | 60 | } 61 | -------------------------------------------------------------------------------- /man/nop.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/class-operation-nop.R, R/operation-nop.R 3 | \docType{class} 4 | \name{nop} 5 | \alias{nop} 6 | \alias{nop-class} 7 | \title{Identity ("no-op") transformation (Layer operation)} 8 | \usage{ 9 | nop(object) 10 | } 11 | \arguments{ 12 | \item{object}{One of: 13 | \itemize{ 14 | \item A \link{layer-like} object: applies this operation to the layer. 15 | \item A missing argument: creates an \link{operation} 16 | }} 17 | } 18 | \value{ 19 | A \link{layer-like} object (if \code{object} is \link{layer-like}) or an \link{operation} (if not). 20 | } 21 | \description{ 22 | A layer \link{operation} which returns the input \link{layer-like} object unchanged. 23 | } 24 | \details{ 25 | When \code{numeric()}s are used with \link{operation}s, they are converted into 26 | sums of \code{nop()}s. 27 | } 28 | \examples{ 29 | library(ggplot2) 30 | 31 | # adding a nop to another operation is equivalent to adding a numeric 32 | adjust() + nop() 33 | 34 | # and vice versa 35 | adjust() + 2 36 | 37 | # here we use adjust() with nop() ( + 1) to create a copy of 38 | # the stat_smooth layer, putting a white outline around it. 39 | set.seed(1234) 40 | k = 1000 41 | data.frame( 42 | x = seq(1, 10, length.out = k), 43 | y = rnorm(k, seq(1, 2, length.out = k) + c(0, 0.5)), 44 | g = c("a", "b") 45 | ) |> 46 | ggplot(aes(x, y, color = g)) + 47 | geom_point() + 48 | stat_smooth(method = lm, formula = y ~ x, linewidth = 1.5, se = FALSE) * 49 | (adjust(aes(group = g), color = "white", linewidth = 4) + 1) + 50 | scale_color_brewer(palette = "Dark2") 51 | 52 | # (note this could also be done with copy_under()) 53 | 54 | } 55 | \seealso{ 56 | \link{operation} for a description of layer operations. 57 | 58 | Other layer operations: 59 | \code{\link{adjust}}, 60 | \code{\link{affine_transform}}, 61 | \code{\link{blend}}, 62 | \code{\link{copy}}, 63 | \code{\link{partition}()} 64 | } 65 | \concept{layer operations} 66 | -------------------------------------------------------------------------------- /man/operation-class.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/class-operation-.R, R/operation-.R, 3 | % R/operation-adjust.R, R/operation-affine-transform.R, R/operation-blend.R, 4 | % R/operation-composition.R, R/operation-nop.R, R/operation-product.R 5 | \docType{class} 6 | \name{operation-class} 7 | \alias{operation-class} 8 | \alias{operation} 9 | \alias{show,operation-method} 10 | \alias{format,operation-method} 11 | \alias{format,adjust-method} 12 | \alias{format,affine_transform-method} 13 | \alias{format,blend-method} 14 | \alias{format,operation_composition-method} 15 | \alias{format,nop-method} 16 | \alias{format,operation_product-method} 17 | \title{Layer operations} 18 | \usage{ 19 | \S4method{show}{operation}(object) 20 | 21 | \S4method{format}{operation}(x, ...) 22 | 23 | \S4method{format}{adjust}(x, ...) 24 | 25 | \S4method{format}{affine_transform}(x, ...) 26 | 27 | \S4method{format}{blend}(x, ...) 28 | 29 | \S4method{format}{operation_composition}(x, ...) 30 | 31 | \S4method{format}{nop}(x, ...) 32 | 33 | \S4method{format}{operation_product}(x, ...) 34 | } 35 | \arguments{ 36 | \item{x, object}{An \link{operation}.} 37 | 38 | \item{...}{Further arguments passed to other methods.} 39 | } 40 | \value{ 41 | For \code{show()}, an \code{\link[=invisible]{invisible()}} copy of the input. 42 | 43 | For \code{format()}, a character string representing the input. 44 | } 45 | \description{ 46 | Layer \link{operation}s are composable transformations that can be applied to \pkg{ggplot2} 47 | \link{layer-like} objects, such as \code{stat}s, \code{geom}s, and lists of \code{stat}s and 48 | \code{geom}s; see the \link{layer-like} documentation page for a description of valid 49 | \link{layer-like} objects. 50 | } 51 | \details{ 52 | \link{operation}s can be composed using the \code{+} and \code{*} operators (see \link{operation_sum} 53 | and \link{operation_product}). Addition and multiplication of \link{operation}s and \link{layer-like} 54 | objects obeys the distributive law. 55 | 56 | \link{operation}s can be applied to \link{layer-like} objects using \code{*} or \verb{|>}, with slightly 57 | different results: 58 | \itemize{ 59 | \item Using \code{*}, application of \link{operation}s to a list of \link{layer-like} objects \emph{is} distributive. For example, 60 | \code{list(geom_line(), geom_point()) * blend("multiply")} is 61 | equivalent to \code{list(geom_line() * blend("multiply"), geom_point() * blend("multiply"))}; 62 | i.e. it multiply-blends the contents of the two layers individually. 63 | \item Using \verb{|>}, application of \link{operation}s to a list of \link{layer-like} objects is \emph{not} 64 | distributive (unless the only reasonable interpretation of applying the 65 | transformation is necessarily distributive; e.g. \code{adjust()}). For example, 66 | \code{list(geom_line(), geom_point()) |> blend("multiply")} would multiply-blend 67 | both layers together, rather than multiply-blending the contents of the 68 | two layers individually. 69 | } 70 | } 71 | \section{Methods (by generic)}{ 72 | \itemize{ 73 | \item \code{show(operation)}: Print an \link{operation}. 74 | 75 | \item \code{format(operation)}: Format an \link{operation} for printing. 76 | 77 | }} 78 | \examples{ 79 | library(ggplot2) 80 | 81 | # operations can stand alone 82 | adjust(aes(color = x)) 83 | 84 | # they can also be applied to layers through multiplication or piping 85 | geom_line() |> adjust(aes(color = x)) 86 | geom_line() * adjust(aes(color = x)) 87 | 88 | # layer operations act as a small algebra, and can be combined through 89 | # multiplication and addition 90 | (adjust(fill = "green") + 1) * blend("multiply") 91 | 92 | } 93 | -------------------------------------------------------------------------------- /man/operation_composition.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/class-operation-composition.R 3 | \docType{class} 4 | \name{operation_composition} 5 | \alias{operation_composition} 6 | \alias{operation_composition-class} 7 | \title{Layer operation composition} 8 | \value{ 9 | An \link{operation}. 10 | } 11 | \description{ 12 | \link{operation}s can be composed together to form chains of operations, which 13 | when multiplied by (applied to) \link{layer-like} objects, return modified \link{layer-like} objects. In 14 | contrast to \link{operation_product}s, compositions of operations are not 15 | distributive over sums of \link{operation}s or \link{layer-like} objects. 16 | } 17 | \details{ 18 | Operation composition is achieved through function application, typically 19 | using the pipe operator (\verb{|>}); e.g. \verb{operation1 |> operation2}. 20 | 21 | The output of composing \pkg{ggblend} \link{operation}s depends on the types of 22 | objects being composed: 23 | \itemize{ 24 | \item If you compose an \link{operation} with an \link{operation}, they are merged into 25 | a single \link{operation} that applies each \link{operation} in sequence, without 26 | distributing over layers. 27 | \item If you compose an \link{operation} with a \link{layer-like} object, that operation is applied 28 | to the layer, returning a new \link{layer-like} object. The operation is applied to the 29 | layer as a whole, not any sub-parts (e.g. sub-layers or graphical objects). 30 | } 31 | } 32 | \examples{ 33 | \dontshow{old_options = options(ggblend.check_blend = FALSE)} 34 | library(ggplot2) 35 | 36 | # composing operations together chains them 37 | adjust(color = "red") |> blend("multiply") 38 | 39 | # unlike multiplication, composition does not follow the distributive law 40 | mult_op = (adjust(aes(y = 11 -x), color = "skyblue") + 1) * blend("multiply") 41 | mult_op 42 | 43 | comp_op = (adjust(aes(y = 11 -x), color = "skyblue") + 1) |> blend("multiply") 44 | comp_op 45 | 46 | # multiplication by a geom returns a modified version of that geom 47 | data.frame(x = 1:10) |> 48 | ggplot(aes(x = x, y = x)) + 49 | geom_line(linewidth = 10, color = "red") * comp_op 50 | \dontshow{options(old_options)} 51 | } 52 | -------------------------------------------------------------------------------- /man/operation_product.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/class-operation-product.R, R/operation-.R, 3 | % R/operation-adjust.R, R/operation-nop.R, R/operation-product.R 4 | \docType{class} 5 | \name{operation_product} 6 | \alias{operation_product} 7 | \alias{operation_product-class} 8 | \alias{*,operation,ANY-method} 9 | \alias{*,ANY,operation-method} 10 | \alias{*,adjust,adjust-method} 11 | \alias{*,nop,nop-method} 12 | \alias{*,operation,nop-method} 13 | \alias{*,operation_sum,nop-method} 14 | \alias{*,nop,operation-method} 15 | \alias{*,nop,operation_sum-method} 16 | \alias{prod,operation-method} 17 | \alias{*,operation,operation-method} 18 | \alias{*,numeric,operation-method} 19 | \alias{*,operation,numeric-method} 20 | \alias{*,operation,operation_sum-method} 21 | \alias{*,operation_sum,operation-method} 22 | \alias{*,operation_sum,operation_sum-method} 23 | \title{Layer operation products} 24 | \usage{ 25 | \S4method{*}{operation,ANY}(e1, e2) 26 | 27 | \S4method{*}{ANY,operation}(e1, e2) 28 | 29 | \S4method{*}{adjust,adjust}(e1, e2) 30 | 31 | \S4method{*}{nop,nop}(e1, e2) 32 | 33 | \S4method{*}{operation,nop}(e1, e2) 34 | 35 | \S4method{*}{operation_sum,nop}(e1, e2) 36 | 37 | \S4method{*}{nop,operation}(e1, e2) 38 | 39 | \S4method{*}{nop,operation_sum}(e1, e2) 40 | 41 | \S4method{prod}{operation}(x, ..., na.rm = FALSE) 42 | 43 | \S4method{*}{operation,operation}(e1, e2) 44 | 45 | \S4method{*}{numeric,operation}(e1, e2) 46 | 47 | \S4method{*}{operation,numeric}(e1, e2) 48 | 49 | \S4method{*}{operation,operation_sum}(e1, e2) 50 | 51 | \S4method{*}{operation_sum,operation}(e1, e2) 52 | 53 | \S4method{*}{operation_sum,operation_sum}(e1, e2) 54 | } 55 | \arguments{ 56 | \item{e1}{an \link{operation}, \link{layer-like}, or \code{\link[=numeric]{numeric()}}} 57 | 58 | \item{e2}{an \link{operation}, \link{layer-like}, or \code{\link[=numeric]{numeric()}}} 59 | 60 | \item{x, ...}{\link{operation}s} 61 | 62 | \item{na.rm}{ignored} 63 | } 64 | \value{ 65 | An \link{operation}. 66 | } 67 | \description{ 68 | \link{operation}s can be multiplied together to form chains of operations, which 69 | when multiplied by (applied to) \link{layer-like} objects, return modified \link{layer-like} objects. 70 | } 71 | \details{ 72 | Multiplication of \pkg{ggblend} \link{operation}s depends on the types of 73 | objects being multiplied: 74 | \itemize{ 75 | \item If you multiply an \link{operation} with an \link{operation}, they are merged into 76 | a single \link{operation} that applies each \link{operation} in sequence. 77 | \item If you multiply an \link{operation} with a \link{layer-like} object, that operation is applied 78 | to the layer, returning a new \link{layer-like} object. 79 | \item If you multiply an \link{operation} by a \code{\link[=numeric]{numeric()}} \emph{n}, a new \link{operation} that 80 | repeats the input \link{operation} is \emph{n} times is returned. 81 | } 82 | } 83 | \examples{ 84 | library(ggplot2) 85 | 86 | # multiplying operations by numerics repeats them... 87 | adjust(color = "red") * 2 88 | 89 | # multiplying operations together chains (or merges) them 90 | adjust(color = "red") * adjust(linewidth = 2) 91 | 92 | # multiplication obeys the distributive law 93 | op = (adjust(aes(y = 11 -x), color = "skyblue") + 1) * (adjust(color = "white", linewidth = 4) + 1) 94 | op 95 | 96 | # multiplication by a geom returns a modified version of that geom 97 | data.frame(x = 1:10) |> 98 | ggplot(aes(x = x, y = x)) + 99 | geom_line(linewidth = 2) * op 100 | 101 | } 102 | -------------------------------------------------------------------------------- /man/operation_sum.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/class-operation-sum.R, R/operation-sum.R 3 | \docType{class} 4 | \name{operation_sum} 5 | \alias{operation_sum} 6 | \alias{operation_sum-class} 7 | \alias{sum,operation-method} 8 | \alias{+,operation,operation-method} 9 | \alias{+,operation,numeric-method} 10 | \alias{+,numeric,operation-method} 11 | \alias{format,operation_sum-method} 12 | \title{Layer operation sums} 13 | \usage{ 14 | \S4method{sum}{operation}(x, ..., na.rm = FALSE) 15 | 16 | \S4method{+}{operation,operation}(e1, e2) 17 | 18 | \S4method{+}{operation,numeric}(e1, e2) 19 | 20 | \S4method{+}{numeric,operation}(e1, e2) 21 | 22 | \S4method{format}{operation_sum}(x, ...) 23 | } 24 | \arguments{ 25 | \item{x, ...}{\link{operation}s} 26 | 27 | \item{na.rm}{ignored} 28 | 29 | \item{e1}{an \link{operation} or \code{\link[=numeric]{numeric()}}} 30 | 31 | \item{e2}{an \link{operation} or \code{\link[=numeric]{numeric()}}} 32 | } 33 | \value{ 34 | An \link{operation}. 35 | } 36 | \description{ 37 | \link{operation}s can be added together to form stacks of operations, which 38 | when multiplied by (applied to) \link{layer-like} objects, those \link{layer-like} objects are distributed 39 | over the \link{operation}s (i.e. copied). 40 | } 41 | \details{ 42 | Addition of \pkg{ggblend} \link{operation}s depends on the types of 43 | objects being summed: 44 | \itemize{ 45 | \item If you add an \link{operation} to an \link{operation}, they are merged into 46 | a single \link{operation} that copies input \link{layer-like} objects, one for each \link{operation}. 47 | \item If you add an \link{operation} to a \code{\link[=numeric]{numeric()}} \emph{n}, it is equivalent to 48 | adding \code{*} \code{\link[=nop]{nop()}}s to that \link{operation}. 49 | } 50 | } 51 | \examples{ 52 | library(ggplot2) 53 | 54 | # adding operations together creates a sum of operations 55 | adjust(color = "red") + adjust(linewidth = 2) 56 | 57 | # addition and multiplication obey the distributive law 58 | op = (adjust(aes(y = 11 -x), color = "skyblue") + 1) * (adjust(color = "white", linewidth = 4) + 1) 59 | op 60 | 61 | # multiplication by a geom returns a modified version of that geom, 62 | # distributed over the sum of the operations 63 | data.frame(x = 1:10) |> 64 | ggplot(aes(x = x, y = x)) + 65 | geom_line(linewidth = 2) * op 66 | } 67 | -------------------------------------------------------------------------------- /man/partition.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/operation-partition.R 3 | \name{partition} 4 | \alias{partition} 5 | \title{Partition a layer into subgroups (Layer operation)} 6 | \usage{ 7 | partition(object, partition) 8 | } 9 | \arguments{ 10 | \item{object}{One of: 11 | \itemize{ 12 | \item A \link{layer-like} object: applies this operation to the layer. 13 | \item A missing argument: creates an \link{operation} 14 | \item Anything else: creates an \link{operation}, passing \code{object} along to the 15 | \code{partition} argument 16 | }} 17 | 18 | \item{partition}{One of: 19 | \itemize{ 20 | \item A list of quosures, such as returned by \code{\link[=vars]{vars()}}, giving a (possibly multi-) 21 | column expression for the \code{partition} aesthetic. These expressions are 22 | combined using \code{\link[=interaction]{interaction()}} to be passed on to \code{aes(partition = ...)} 23 | \item A one-sided formula, giving a single-column expression for the \code{partition} 24 | aesthetic, which is passed on to \code{aes_(partition = ...)}. 25 | }} 26 | } 27 | \value{ 28 | A \link{layer-like} object (if \code{object} is \link{layer-like}) or an \link{operation} (if not). 29 | } 30 | \description{ 31 | A layer \link{operation} for adding a \code{partition} aesthetic to a \link{layer}. 32 | } 33 | \details{ 34 | This is a shortcut for setting the \code{partition} aesthetic of a \link{layer}. 35 | \itemize{ 36 | \item \code{partition(~ XXX)} is roughly equivalent to \code{adjust(aes(partition = XXX))} 37 | \item \code{partition(vars(X, Y, ...))} is roughly equivalent to \code{adjust(aes(partition = interaction(X, Y, ...)))} 38 | } 39 | 40 | When a \link{layer} with a \code{partition} aesthetic is used by the following 41 | \link{operation}s, the effects of the operations are applied across groups: 42 | \itemize{ 43 | \item \code{\link[=blend]{blend()}}: Blends graphical objects within the subgroups defined by the 44 | partition together using normal (\code{"over"}) blending before applying its 45 | blend between subgroups. 46 | } 47 | } 48 | \examples{ 49 | \dontshow{old_options = options(ggblend.check_blend = FALSE)} 50 | library(ggplot2) 51 | 52 | # create two versions of a dataset, where draw order can affect output 53 | set.seed(1234) 54 | df_a = data.frame(x = rnorm(500, 0), y = rnorm(500, 1), set = "a") 55 | df_b = data.frame(x = rnorm(500, 1), y = rnorm(500, 2), set = "b") 56 | df_ab = rbind(df_a, df_b) |> 57 | transform(order = "draw a then b") 58 | df_ba = rbind(df_b, df_a) |> 59 | transform(order = "draw b then a") 60 | df = rbind(df_ab, df_ba) 61 | 62 | # Using the "multiply" blend mode, draw order does not matter, but 63 | # the "multiply" blend is applied to all points, creating dark regions 64 | # outside the intersection: 65 | df |> 66 | ggplot(aes(x, y, color = set)) + 67 | geom_point(size = 3, alpha = 0.5) |> blend("multiply") + 68 | scale_color_brewer(palette = "Set1") + 69 | facet_grid(~ order) 70 | 71 | # By partitioning (either through |> partition(vars(set)) or aes(partition = set)) 72 | # we will blend using the default blend mode (over) first, then we can apply the 73 | # "multiply" blend just between the two sets, so the regions outside the 74 | # intersection are not blended using "multiply": 75 | df |> 76 | ggplot(aes(x, y, color = set, partition = set)) + 77 | geom_point(size = 3, alpha = 0.5) |> blend("multiply") + 78 | scale_color_brewer(palette = "Set1") + 79 | facet_grid(~ order) 80 | \dontshow{options(old_options)} 81 | } 82 | \seealso{ 83 | \link{operation} for a description of layer operations. 84 | 85 | Other layer operations: 86 | \code{\link{adjust}}, 87 | \code{\link{affine_transform}}, 88 | \code{\link{blend}}, 89 | \code{\link{copy}}, 90 | \code{\link{nop}} 91 | } 92 | \concept{layer operations} 93 | -------------------------------------------------------------------------------- /pkgdown/extra.scss: -------------------------------------------------------------------------------- 1 | 2 | @media (min-width: 1400px) { 3 | body { 4 | font-size: inherit; 5 | } 6 | } 7 | 8 | h6,.h6,h5,.h5,h4,.h4,h3,.h3,h2,.h2,h1,.h1 { 9 | margin-top: 2rem; 10 | margin-bottom: 1rem; 11 | font-weight: 600; 12 | color: #444444; 13 | } 14 | 15 | pre { 16 | padding: 0.9rem 1rem 1rem; 17 | background-color: #f6f6f6; 18 | border: none; 19 | border-radius: 0.5em; 20 | } 21 | 22 | .navbar { 23 | background-color: white !important; 24 | box-shadow: 0 1px 2px rgba(0,0,0,0.3); 25 | } 26 | 27 | .navbar-brand { 28 | font-size: var(--bs-nav-link-font-size); 29 | color: #6c757d; 30 | } 31 | 32 | h1, .h1 { 33 | font-size: calc(1.375rem + 1vw); 34 | font-weight: 700; 35 | } 36 | 37 | h2, .h2 { 38 | font-size: calc(1.325rem + 0.5vw); 39 | } 40 | 41 | @media (min-width: 1200px) { 42 | h1, .h1 { 43 | font-size: 2rem; 44 | } 45 | 46 | h2, .h2 { 47 | font-size: 1.75rem; 48 | } 49 | } 50 | 51 | aside h2, aside .h2 { 52 | margin-bottom: 0.5rem; 53 | } 54 | -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggblend/1c08d8767a113ddc86634d41b353b15d71e34126/pkgdown/favicon/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggblend/1c08d8767a113ddc86634d41b353b15d71e34126/pkgdown/favicon/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggblend/1c08d8767a113ddc86634d41b353b15d71e34126/pkgdown/favicon/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggblend/1c08d8767a113ddc86634d41b353b15d71e34126/pkgdown/favicon/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggblend/1c08d8767a113ddc86634d41b353b15d71e34126/pkgdown/favicon/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggblend/1c08d8767a113ddc86634d41b353b15d71e34126/pkgdown/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggblend/1c08d8767a113ddc86634d41b353b15d71e34126/pkgdown/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggblend/1c08d8767a113ddc86634d41b353b15d71e34126/pkgdown/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggblend/1c08d8767a113ddc86634d41b353b15d71e34126/pkgdown/favicon/favicon.ico -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(ggblend) 3 | 4 | test_check("ggblend") 5 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/operation-/ggnewscale_with_blend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggblend/1c08d8767a113ddc86634d41b353b15d71e34126/tests/testthat/_snaps/operation-/ggnewscale_with_blend.png -------------------------------------------------------------------------------- /tests/testthat/_snaps/operation-affine-transform/affine_transform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggblend/1c08d8767a113ddc86634d41b353b15d71e34126/tests/testthat/_snaps/operation-affine-transform/affine_transform.png -------------------------------------------------------------------------------- /tests/testthat/_snaps/operation-affine-transform/affine_transform_on_two_layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggblend/1c08d8767a113ddc86634d41b353b15d71e34126/tests/testthat/_snaps/operation-affine-transform/affine_transform_on_two_layers.png -------------------------------------------------------------------------------- /tests/testthat/_snaps/operation-blend/complex_blend_sequence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggblend/1c08d8767a113ddc86634d41b353b15d71e34126/tests/testthat/_snaps/operation-blend/complex_blend_sequence.png -------------------------------------------------------------------------------- /tests/testthat/_snaps/operation-blend/multiply_blend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggblend/1c08d8767a113ddc86634d41b353b15d71e34126/tests/testthat/_snaps/operation-blend/multiply_blend.png -------------------------------------------------------------------------------- /tests/testthat/_snaps/operation-blend/multiply_blend_with_partition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggblend/1c08d8767a113ddc86634d41b353b15d71e34126/tests/testthat/_snaps/operation-blend/multiply_blend_with_partition.png -------------------------------------------------------------------------------- /tests/testthat/_snaps/operation-blend/multiply_blend_without_partition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjskay/ggblend/1c08d8767a113ddc86634d41b353b15d71e34126/tests/testthat/_snaps/operation-blend/multiply_blend_without_partition.png -------------------------------------------------------------------------------- /tests/testthat/helper-expect-snapshot-plot.R: -------------------------------------------------------------------------------- 1 | save_png = function(fig, width = 500, height = 500) { 2 | path = tempfile(fileext = ".png") 3 | 4 | # use Liberation Sans and Symbola to avoid platform-specific font differences 5 | liberation_sans = fontquiver::font_styles("Liberation", "Sans") 6 | symbola = fontquiver::font("Symbola", "Symbols", "Regular") 7 | sysfonts::font_add( 8 | "Liberation Sans", 9 | regular = liberation_sans$Regular$ttf, 10 | bold = liberation_sans$Bold$ttf, 11 | italic = liberation_sans$Italic$ttf, 12 | bolditalic = liberation_sans$`Bold Italic`$ttf, 13 | symbol = symbola$ttf 14 | ) 15 | 16 | png(path, width = width, height = height, type = "cairo") 17 | showtext::showtext_begin() 18 | on.exit({ 19 | showtext::showtext_end() 20 | grDevices::dev.off() 21 | }) 22 | 23 | print(fig) 24 | 25 | path 26 | } 27 | 28 | expect_snapshot_plot = function(title, fig) { 29 | # Announce the files for before skipping and before touching `fig`. 30 | # This way, if `fig` unexpectedly fails or skips (or is not run on this platform), 31 | # testthat will not auto-delete the corresponding snapshot file. 32 | file_name = paste0(gsub("\\.", "_", make.names(title)), ".png") 33 | announce_snapshot_file(file_name) 34 | 35 | skip_on_cran() 36 | skip_on_os("mac") # these tests only work on windows and linux 37 | skip_if_not_installed("fontquiver") 38 | skip_if_not_installed("sysfonts") 39 | skip_if_not_installed("showtext") 40 | 41 | # void theme + no panel labels to reduce the amount of potential text / 42 | # elements which may be more likely to vary by OS 43 | path = save_png(fig + theme_void() + theme(panel.border = element_rect(fill = NA), strip.text = element_blank())) 44 | expect_snapshot_file(path, file_name) 45 | } 46 | -------------------------------------------------------------------------------- /tests/testthat/helper-grob.R: -------------------------------------------------------------------------------- 1 | unname_grob = function(x) { 2 | if (is.list(x)) { 3 | out = sapply(x, unname_grob, simplify = FALSE) 4 | class(out) = class(x) 5 | } else { 6 | out = x 7 | } 8 | if (inherits(out, c("grob", "viewport"))) { 9 | out$name = NULL 10 | out$childrenOrder = NULL 11 | names(out$children) = NULL 12 | out$group = NULL 13 | } 14 | out 15 | } 16 | 17 | #' used for comparing grobs. Grobs may not appear to be exactly equal because 18 | #' of names; this expectation fixes that 19 | expect_equal_grob = function(object, expected) { 20 | label = as_label(enquo(object)) 21 | expected.label = as_label(enquo(expected)) 22 | 23 | expect_equal( 24 | unname_grob(object), unname_grob(expected), 25 | label = label, 26 | expected.label = expected.label 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /tests/testthat/helper-layer.R: -------------------------------------------------------------------------------- 1 | layer_to_list = function(x) { 2 | x = if (is.list(x)) { 3 | lapply(x, layer_to_list) 4 | } else { 5 | as.list(x) 6 | } 7 | # the `constructor` element of a layer may be different depending on the 8 | # operations that produced it, and we don't care about this when checking 9 | # for equality 10 | x$constructor = NULL 11 | x 12 | } 13 | 14 | #' used for comparing layers. Layers (as ggproto objects) may not appear to be 15 | #' exactly equal because the definitions of variables may be in superclasses; 16 | #' this expectation fixes that 17 | expect_equal_layer = function(object, expected) { 18 | label = as_label(enquo(object)) 19 | expected.label = as_label(enquo(expected)) 20 | 21 | expect_equal( 22 | class(object), class(expected), 23 | label = paste0("class(", label, ")"), 24 | expected.label = paste0("class(", expected.label, ")") 25 | ) 26 | expect_equal( 27 | layer_to_list(object), layer_to_list(expected), 28 | label = label, 29 | expected.label = expected.label 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /tests/testthat/helper-warnings.R: -------------------------------------------------------------------------------- 1 | #' helper for running tests without warnings about graphics device support 2 | without_warnings = function(expr) { 3 | old_options = options( 4 | "ggblend.check_blend" = FALSE, 5 | "ggblend.check_affine_transform" = FALSE 6 | ) 7 | 8 | expr 9 | 10 | options(old_options) 11 | } 12 | 13 | #' helper for running tests in a graphics device that should throw warnings 14 | with_old_graphics_device = function(expr) { 15 | path = tempfile(fileext = ".tex") 16 | pictex(path) 17 | on.exit(dev.off()) 18 | 19 | expr 20 | } 21 | -------------------------------------------------------------------------------- /tests/testthat/test-layer-list.R: -------------------------------------------------------------------------------- 1 | 2 | # casting ----------------------------------------------------------------- 3 | 4 | test_that("basic casting works", { 5 | expect_equal(as_layer_list(geom_line()), layer_list(geom_line())) 6 | expect_equal(as_layer_list(list()), layer_list()) 7 | expect_equal(as_layer_list(layer_list(geom_line())), layer_list(geom_line())) 8 | expect_equal(layer_list(geom_line()) + layer_list(geom_point()), layer_list(geom_line(), geom_point())) 9 | 10 | expect_error(as_layer_list(list("a")), "All objects in a layer_list must be layer-like") 11 | }) 12 | 13 | 14 | # printing ---------------------------------------------------------------- 15 | 16 | test_that("basic printing works", { 17 | expect_output(print(layer_list(geom_blank())), 18 | paste( 19 | c(":", capture.output(print(list(geom_blank())))), 20 | collapse = "\n"), 21 | fixed = TRUE 22 | ) 23 | }) 24 | -------------------------------------------------------------------------------- /tests/testthat/test-operation-.R: -------------------------------------------------------------------------------- 1 | 2 | 3 | # operation algebra works (without layers) -------------------------------- 4 | 5 | test_that("distributive law works", { 6 | expect_equal( 7 | (adjust(color = "red") + 1) * adjust(size = 2), 8 | new_operation_sum(list(adjust(color = "red", size = 2), adjust(size = 2))) 9 | ) 10 | 11 | expect_equal( 12 | adjust(size = 2) * (1 + adjust(color = "red")), 13 | new_operation_sum(list(adjust(size = 2), adjust(size = 2, color = "red"))) 14 | ) 15 | 16 | expect_equal( 17 | (adjust(color = "red") + 1) * (1 + adjust(size = 2)), 18 | new_operation_sum(list( 19 | adjust(color = "red"), adjust(color = "red", size = 2), 20 | nop(), adjust(size = 2) 21 | )) 22 | ) 23 | }) 24 | 25 | test_that("addition with 0 works", { 26 | expect_equal(0 + adjust(size = 2), new_operation_sum(list(adjust(size = 2)))) 27 | expect_equal(adjust(size = 2) + 0, new_operation_sum(list(adjust(size = 2)))) 28 | expect_equal(0 + adjust(size = 2) + 0, new_operation_sum(list(adjust(size = 2)))) 29 | }) 30 | 31 | test_that("multiplication by 0 works", { 32 | expect_equal(0 * adjust(size = 2), new_operation_sum(list())) 33 | expect_equal(adjust(size = 2) * 0, new_operation_sum(list())) 34 | }) 35 | 36 | test_that("multiplication by 1 works", { 37 | expect_equal(1 * adjust(size = 2), new_operation_sum(list(adjust(size = 2)))) 38 | expect_equal(adjust(size = 2) * 1, new_operation_sum(list(adjust(size = 2)))) 39 | }) 40 | 41 | test_that("multiplication by n > 1 works", { 42 | expect_equal(adjust(size = 2) * 2, new_operation_sum(list(adjust(size = 2), adjust(size = 2)))) 43 | expect_equal(2 * adjust(size = 2), new_operation_sum(list(adjust(size = 2), adjust(size = 2)))) 44 | }) 45 | 46 | test_that("multiplying by a layer list works", { 47 | expect_equal(layer_list() * adjust(), layer_list()) 48 | }) 49 | 50 | 51 | # casting ----------------------------------------------------------------- 52 | 53 | test_that("converting a list to an operation works", { 54 | expect_equal(as(list(1, adjust(), adjust() + blend()), "operation"), nop() + adjust() + adjust() + blend()) 55 | }) 56 | 57 | 58 | # non-layers throw errors ----------------------------------------------- 59 | 60 | test_that("multiplying by a non-layer throws an error", { 61 | expect_error("a" * adjust(), r'(Cannot\s+convert\s+object.*to\s+a\s+layer-like)') 62 | }) 63 | 64 | 65 | # printing ---------------------------------------------------------------- 66 | 67 | test_that("print works", { 68 | expect_output(print(blend()), ": blend()") 69 | }) 70 | 71 | 72 | # operation construction -------------------------------------------------- 73 | 74 | test_that("operation construction and printing works", { 75 | setClass("test_operation", slots = list(x = "ANY", y = "ANY"), contains = "operation") 76 | new_test_operation = function(x = 0, y = 0) { 77 | new("test_operation", x = x, y = y) 78 | } 79 | test_operation = make_operation("test_operation", new_test_operation, x) 80 | expect_equal(format(test_operation()), "test_operation(x = 0, y = 0)") 81 | expect_equal(format(test_operation(x = 3, y = 2)), "test_operation(x = 3, y = 2)") 82 | expect_equal(format(adjust() |> test_operation()), "adjust() |> test_operation(x = 0, y = 0)") 83 | expect_equal(format(test_operation(1)), "test_operation(x = 1, y = 0)") 84 | expect_error(test_operation(0, x = 0), r'(Cannot\s+provide\s+both.*arguments)') 85 | expect_error(geom_blank() |> test_operation(), "Unimplemented layer operation") 86 | expect_error(geom_blank() * test_operation(), "Unimplemented layer operation") 87 | }) 88 | 89 | 90 | # compatibility ----------------------------------------------------------- 91 | 92 | test_that("ggnewscale works with blend", { 93 | skip_if_not_installed("ggnewscale") 94 | 95 | expect_snapshot_plot("ggnewscale with blend", 96 | data.frame(x = 1, y = 1, g = c("a","b"), h = c("d", "e")) |> 97 | ggplot(aes(x, y, shape = g)) + 98 | list( 99 | geom_point(aes(color = g), size = 20, show.legend = FALSE), 100 | scale_color_brewer(palette = "Set1"), 101 | ggnewscale::new_scale_color(), 102 | geom_point(aes(color = h, x = 1.5), size = 20, show.legend = FALSE) 103 | ) * blend("multiply") + 104 | scale_color_brewer(palette = "Set2") + 105 | coord_cartesian(xlim = c(0.5, 2)) 106 | ) 107 | }) 108 | -------------------------------------------------------------------------------- /tests/testthat/test-operation-adjust.R: -------------------------------------------------------------------------------- 1 | 2 | # operation application --------------------------------------------------- 3 | 4 | test_that("adjust works as an identity", { 5 | expect_equal_layer(adjust() * geom_line(), geom_line()) 6 | expect_equal_layer(geom_line() * adjust(), geom_line()) 7 | expect_equal_layer(geom_line() |> adjust(), geom_line()) 8 | }) 9 | 10 | test_that("adjusting params works", { 11 | expect_equal_layer(adjust(bins = 20) * stat_summary_bin(), stat_summary_bin(bins = 20)) 12 | expect_equal_layer(stat_summary_bin() * adjust(fatten = 5), stat_summary_bin(fatten = 5)) 13 | expect_equal_layer(stat_summary_bin(fatten = 3) |> adjust(fatten = 5), stat_summary_bin(fatten = 5)) 14 | }) 15 | 16 | test_that("adjusting aesthetics works", { 17 | expect_equal_layer(adjust(aes(color = z)) * stat_summary_bin(), stat_summary_bin(aes(color = z))) 18 | expect_equal_layer(stat_summary_bin() * adjust(mapping = aes(color = z)), stat_summary_bin(aes(color = z))) 19 | expect_equal_layer(stat_summary_bin(aes(color = g)) |> adjust(aes(color = z, fill = y)), stat_summary_bin(aes(color = z, fill = y))) 20 | }) 21 | 22 | 23 | # self-multiplication ---------------------------------------------------------- 24 | 25 | test_that("adjust multiplied with itself merges into one adjust", { 26 | expect_equal(adjust() * adjust(), adjust()) 27 | expect_equal(adjust(size = 2) * adjust(size = 3), adjust(size = 3)) 28 | expect_equal(adjust(size = 2) * adjust(color = "red"), adjust(size = 2, color = "red")) 29 | expect_equal((1 + adjust(size = 2)) * adjust(color = "red"), adjust(color = "red") + adjust(size = 2, color = "red")) 30 | }) 31 | 32 | -------------------------------------------------------------------------------- /tests/testthat/test-operation-affine-transform.R: -------------------------------------------------------------------------------- 1 | test_that("transforming works", { 2 | expect_snapshot_plot("affine transform", 3 | data.frame(x = 1, y = 0.5) |> 4 | ggplot(aes(x, y)) + 5 | geom_point(size = 10, shape = 15, alpha = 0.5) + 6 | geom_point(size = 10, shape = 15, color = "red", alpha = 0.5) |> 7 | affine_transform(x = unit(10, "pt"), y = unit(20, "pt"), width = 0.5, height = 2, angle = 15) + 8 | guides(color = "none", shape = "none") 9 | ) 10 | }) 11 | 12 | test_that("transforming two layers", { 13 | expect_snapshot_plot("affine transform on two layers", 14 | data.frame(x = 1, y = 0.25) |> 15 | ggplot(aes(x, y)) + 16 | geom_point(size = 10, shape = 15, alpha = 0.5) + 17 | geom_point(aes(y = 0.75), size = 10, shape = 15, alpha = 0.5) + 18 | list( 19 | geom_point(size = 10, shape = 15, color = "red", alpha = 0.5), 20 | geom_point(aes(y = 0.75), size = 10, color = "red", shape = 15, alpha = 0.5) 21 | )|> 22 | affine_transform(x = unit(10, "pt"), y = unit(-10, "pt"), width = 1, height = 1, angle = 0) + 23 | guides(color = "none", shape = "none") + 24 | scale_y_continuous(limits = c(0, 1)) 25 | ) 26 | }) 27 | 28 | 29 | # argument checks --------------------------------------------------------- 30 | 31 | test_that("unit arguments are checked", { 32 | expect_error(affine_transform(x = "a"), r"(must\s+be\s+a\s+numeric\s+or\s+a\s+grid::unit)") 33 | expect_error(affine_transform(y = "a"), r"(must\s+be\s+a\s+numeric\s+or\s+a\s+grid::unit)") 34 | }) 35 | 36 | 37 | # empty grobs ------------------------------------------------------------- 38 | 39 | test_that("transforming an empty grob works", { 40 | without_warnings({ 41 | p = ggplot() + geom_blank() |> affine_transform(x = 1, y = 0, width = 0.5, height = 2, angle = 10) 42 | 43 | zg = defineGrob(zeroGrob()) 44 | ref = list(grobTree( 45 | zg, 46 | grobTree( 47 | useGrob(zg$name), 48 | vp = viewport( 49 | x = unit(1.5, "npc"), y = unit(0.5, "npc"), 50 | width = 0.5, height = 2, angle = 10 51 | ) 52 | ) 53 | )) 54 | expect_equal_grob(layer_grob(p, 1), ref) 55 | }) 56 | }) 57 | 58 | 59 | # printing ---------------------------------------------------------------- 60 | 61 | test_that("format works", { 62 | expect_equal(format(affine_transform()), "affine_transform()") 63 | expect_equal(format(affine_transform(x = 0)), "affine_transform()") 64 | expect_equal(format(affine_transform(x = 1)), 'affine_transform(x = 1npc)') 65 | expect_equal(format(affine_transform(x = 1, y = unit(2, "pt"))), 'affine_transform(x = 1npc, y = 2points)') 66 | }) 67 | 68 | 69 | # affine transform capabilities warning ----------------------------------- 70 | 71 | test_that("affine transform warning works", { 72 | with_old_graphics_device({ 73 | expect_warning(layer_grob(ggplot() + geom_blank() |> affine_transform()), 74 | r"(Your\s+graphics\s+device.+reports\s+that\s+affine\s+transformations\s+are\s+not\s+supported)" 75 | ) 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /tests/testthat/test-operation-blend.R: -------------------------------------------------------------------------------- 1 | test_that("basic blending works", { 2 | expect_snapshot_plot("multiply blend", 3 | data.frame(x = c(1,2,2,3), g = c("a", "a", "b", "b")) |> 4 | ggplot(aes(x, x, color = g, shape = g)) + 5 | geom_point(size = 10) * blend("multiply") + 6 | guides(color = "none", shape = "none") 7 | ) 8 | }) 9 | 10 | test_that("blending without partition works", { 11 | expect_snapshot_plot("multiply blend without partition", 12 | data.frame(x = c(1,1.98,2,2.02,2.5,2.52,3), g = c("a", "a", "b", "b", "b", "b", "b")) |> 13 | ggplot(aes(x, x, color = g, shape = g)) + 14 | geom_point(size = 10) |> blend("multiply")+ 15 | guides(color = "none", shape = "none") 16 | ) 17 | }) 18 | 19 | test_that("blending with partition works", { 20 | expect_snapshot_plot("multiply blend with partition", 21 | data.frame(x = c(1,1.98,2,2.02,2.5,2.52,3), g = c("a", "a", "b", "b", "b", "b", "b")) |> 22 | ggplot(aes(x, x, color = g, shape = g)) + 23 | geom_point(size = 10) |> partition(vars(g)) |> blend("multiply") + 24 | guides(color = "none", shape = "none") 25 | ) 26 | }) 27 | 28 | test_that("complex sequence of blends works", { 29 | set.seed(1234) 30 | df_a = data.frame(x = rnorm(100, 0), y = rnorm(100, 1), set = "a") 31 | df_b = data.frame(x = rnorm(100, 1), y = rnorm(100, 2), set = "b") 32 | 33 | df_ab = rbind(df_a, df_b) |> 34 | transform(order = "draw a then b") 35 | 36 | df_ba = rbind(df_b, df_a) |> 37 | transform(order = "draw b then a") 38 | 39 | df = rbind(df_ab, df_ba) 40 | 41 | expect_snapshot_plot("complex blend sequence", 42 | df |> 43 | ggplot(aes(x, y, color = set)) + 44 | list( 45 | # double blend here since it may not always work 46 | geom_point(size = 6) * (blend("lighten") + blend("multiply", alpha = 0.65)) |> blend() * blend(), 47 | geom_vline(xintercept = 0, color = "gray75", linewidth = 1.5), 48 | geom_hline(yintercept = 0, color = "gray75", linewidth = 1.5) 49 | ) |> blend("hard.light") + 50 | scale_color_brewer(palette = "Set2") + 51 | guides(color = "none", shape = "none") + 52 | facet_grid(~ order) 53 | ) 54 | }) 55 | 56 | 57 | # empty grobs ------------------------------------------------------------- 58 | 59 | test_that("blending an empty grob works", { 60 | without_warnings({ 61 | p = ggplot() + geom_blank() |> blend("multiply") 62 | 63 | expect_equal_grob(layer_grob(p, 1), list(groupGrob(zeroGrob(), "multiply"))) 64 | }) 65 | }) 66 | 67 | test_that("blending a list of empty grobs works", { 68 | without_warnings({ 69 | p = ggplot() + list(geom_blank(), geom_blank()) |> blend("multiply") 70 | 71 | expect_equal_grob(layer_grob(p, 1), list(zeroGrob())) 72 | expect_equal_grob(layer_grob(p, 2), list(zeroGrob())) 73 | 74 | expect_equal_grob(layer_grob(p, 3), 75 | list(groupGrob( 76 | grobTree( 77 | groupGrob(zeroGrob()), 78 | groupGrob(zeroGrob()) 79 | ), 80 | "multiply" 81 | )) 82 | ) 83 | }) 84 | }) 85 | 86 | 87 | # printing ---------------------------------------------------------------- 88 | 89 | test_that("format works", { 90 | expect_equal(format(blend()), "blend()") 91 | expect_equal(format(blend("over")), "blend()") 92 | expect_equal(format(blend("multiply")), 'blend("multiply")') 93 | expect_equal(format(blend(alpha = 0.1)), 'blend(alpha = 0.1)') 94 | expect_equal(format(blend("multiply", alpha = 0.1)), 'blend("multiply", alpha = 0.1)') 95 | }) 96 | 97 | 98 | # blend capabilities warning ---------------------------------------------- 99 | 100 | test_that("blend warning works", { 101 | with_old_graphics_device({ 102 | expect_warning(layer_grob(ggplot() + geom_blank() |> blend("multiply")), 103 | r"(Your\s+graphics\s+device.+reports\s+that\s+blend.+is\s+not\s+supported)" 104 | ) 105 | }) 106 | }) 107 | -------------------------------------------------------------------------------- /tests/testthat/test-operation-composition.R: -------------------------------------------------------------------------------- 1 | # printing ---------------------------------------------------------------- 2 | 3 | test_that("format works", { 4 | expect_equal(format(new_operation_composition(adjust(), blend())), "adjust() |> blend()") 5 | }) 6 | -------------------------------------------------------------------------------- /tests/testthat/test-operation-copy.R: -------------------------------------------------------------------------------- 1 | 2 | # basic copies -------------------------------------------------------------- 3 | 4 | test_that("basic copy operations", { 5 | expect_equal(copy_over(aes(x = 1), color = "red"), 1 + adjust(aes(x = 1), color = "red")) 6 | expect_equal(copy_under(aes(x = 1), color = "red"), adjust(aes(x = 1), color = "red") + 1) 7 | }) 8 | -------------------------------------------------------------------------------- /tests/testthat/test-operation-nop.R: -------------------------------------------------------------------------------- 1 | test_that("nop works", { 2 | expect_equal(nop() * nop(), nop()) 3 | expect_equal(nop() * geom_line(), geom_line()) 4 | expect_equal(geom_line() * nop(), geom_line()) 5 | expect_equal(geom_line() |> nop(), geom_line()) 6 | expect_equal(nop() * (nop() + nop()), nop() + nop()) 7 | expect_equal(nop() * 2, nop() + nop()) 8 | expect_equal((adjust() + blend()) * nop(), adjust() + blend()) 9 | }) 10 | 11 | # printing ---------------------------------------------------------------- 12 | 13 | test_that("format works", { 14 | expect_equal(format(nop()), "1") 15 | }) 16 | -------------------------------------------------------------------------------- /tests/testthat/test-operation-partition.R: -------------------------------------------------------------------------------- 1 | 2 | # basic partition -------------------------------------------------------------- 3 | 4 | test_that("basic partitions", { 5 | expect_equal(partition(vars(a)), adjust(aes(partition = a))) 6 | expect_equal( 7 | partition(vars(a, b)), 8 | adjust(aes(partition = interaction(!!quo(a), !!quo(b), drop = TRUE, lex.order = TRUE))), 9 | ignore_formula_env = TRUE 10 | ) 11 | expect_equal(partition(~ a), adjust(aes(partition = a))) 12 | expect_equal(partition(~ a + b), adjust(aes(partition = a + b))) 13 | expect_equal( 14 | partition(quote(a)), 15 | adjust(aes(partition = a)), 16 | ignore_formula_env = TRUE 17 | ) 18 | }) 19 | -------------------------------------------------------------------------------- /tests/testthat/test-operation-product.R: -------------------------------------------------------------------------------- 1 | 2 | # basic products -------------------------------------------------------------- 3 | 4 | test_that("basic products work", { 5 | expect_equal( 6 | adjust(color = "red") * blend(), 7 | new_operation_product(list(adjust(color = "red"), blend())) 8 | ) 9 | }) 10 | 11 | test_that("prod() works", { 12 | expect_equal(prod(adjust(), blend(), adjust(color = "red")), adjust() * blend() * adjust(color = "red")) 13 | }) 14 | 15 | test_that("products of operations are applied correctly", { 16 | expect_equal( 17 | geom_point() * (adjust(aes(color = "red")) * blend("multiply")), 18 | geom_point() |> adjust(aes(color = "red")) |> blend("multiply") 19 | ) 20 | expect_equal( 21 | geom_point() * adjust(aes(color = "red")) * blend("multiply"), 22 | geom_point() |> adjust(aes(color = "red")) |> blend("multiply") 23 | ) 24 | }) 25 | 26 | 27 | # casting ----------------------------------------------------------------- 28 | 29 | test_that("converting a list to an operation product works", { 30 | expect_equal(as(list(adjust(), blend()), "operation_product"), adjust() * blend()) 31 | }) 32 | 33 | 34 | # printing ---------------------------------------------------------------- 35 | 36 | test_that("format works", { 37 | expect_equal(format(new_operation_product(list())), "0") 38 | expect_equal(format(new_operation_product(list(adjust()))), "adjust()") 39 | expect_equal(format(new_operation_product(list(adjust(), blend()))), "adjust() * blend()") 40 | }) 41 | -------------------------------------------------------------------------------- /tests/testthat/test-operation-sum.R: -------------------------------------------------------------------------------- 1 | 2 | # basic sums -------------------------------------------------------------- 3 | 4 | test_that("basic sums work", { 5 | expect_equal( 6 | adjust(color = "red") + 1 + adjust(size = 2) + 0, 7 | new_operation_sum(list(adjust(color = "red"), nop(), adjust(size = 2))) 8 | ) 9 | }) 10 | 11 | test_that("sum() works", { 12 | expect_equal(sum(nop(), adjust(), blend()), nop() + adjust() + blend()) 13 | }) 14 | 15 | 16 | # casting ----------------------------------------------------------------- 17 | 18 | test_that("converting a list to an operation sum works", { 19 | expect_equal(as(list(1, adjust(), adjust() + blend()), "operation_sum"), nop() + adjust() + adjust() + blend()) 20 | }) 21 | 22 | 23 | # multiplication of sums -------------------------------------------------------------- 24 | 25 | test_that("multiplication of sums works", { 26 | expect_equal( 27 | (adjust(color = "red") + 1 + 1) * (adjust(size = 2) + 1), 28 | new_operation_sum(list( 29 | adjust(color = "red", size = 2), adjust(color = "red"), 30 | adjust(size = 2), nop(), 31 | adjust(size = 2), nop() 32 | )) 33 | ) 34 | }) 35 | 36 | 37 | # operation application --------------------------------------------------- 38 | 39 | test_that("application of sums preserves structure of input", { 40 | input = list( 41 | geom_line(), 42 | geom_path(), 43 | list( 44 | geom_bar(), 45 | geom_col() 46 | ), 47 | list( 48 | geom_histogram() 49 | ) 50 | ) 51 | 52 | ref = layer_list( 53 | list(geom_line(color = "red"), geom_line(linewidth = 2)), 54 | list(geom_path(color = "red"), geom_path(linewidth = 2)), 55 | list( 56 | list(geom_bar(color = "red"), geom_bar(linewidth = 2)), 57 | list(geom_col(color = "red"), geom_col(linewidth = 2)) 58 | ), 59 | list( 60 | list(geom_histogram(color = "red"), geom_histogram(linewidth = 2)) 61 | ) 62 | ) 63 | 64 | expect_equal_layer( 65 | input * (adjust(color = "red") + adjust(linewidth = 2)), 66 | ref 67 | ) 68 | }) 69 | 70 | 71 | # printing ---------------------------------------------------------------- 72 | 73 | test_that("format works", { 74 | expect_equal(format(new_operation_sum(list())), "0") 75 | expect_equal(format(new_operation_sum(list(adjust()))), "adjust()") 76 | expect_equal(format(new_operation_sum(list(adjust(), nop()))), "(adjust() + 1)") 77 | }) 78 | --------------------------------------------------------------------------------