├── .github ├── .gitignore └── workflows │ ├── R-CMD-check.yaml │ └── rhub.yaml ├── .gitignore ├── tests ├── testthat.R └── testthat │ ├── test_gif.R │ ├── test_construct.R │ ├── test_methods.R │ ├── test_Ops.R │ └── test_data.R ├── README_supp ├── scooby.gif ├── mycoolplot.gif ├── mycoolplot.rds ├── README-unnamed-chunk-10-1.png ├── README-unnamed-chunk-11-1.png ├── README-unnamed-chunk-12-1.png ├── README-unnamed-chunk-12-2.png ├── README-unnamed-chunk-13-1.png ├── README-unnamed-chunk-13-2.png ├── README-unnamed-chunk-14-1.png ├── README-unnamed-chunk-16-1.png ├── README-unnamed-chunk-17-1.png ├── README-unnamed-chunk-18-1.png ├── README-unnamed-chunk-19-1.png ├── README-unnamed-chunk-8-1.png └── README-unnamed-chunk-9-1.png ├── cran-comments.md ├── .Rbuildignore ├── R ├── zzz.R ├── ggghost-package.R └── ghost.R ├── man ├── is.ggghost.Rd ├── supp_data.Rd ├── print.ggghost.Rd ├── supp_data-set.Rd ├── plus-ggghost.Rd ├── grapes-g-less-than-grapes.Rd ├── identify_data.Rd ├── recover_data.Rd ├── subset.ggghost.Rd ├── summary.ggghost.Rd ├── reanimate.Rd ├── ggghost-package.Rd └── minus-ggghost.Rd ├── NAMESPACE ├── DESCRIPTION ├── NEWS.md ├── README.Rmd └── README.md /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | ggghost.Rproj 5 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(ggghost) 3 | 4 | test_check("ggghost") 5 | -------------------------------------------------------------------------------- /README_supp/scooby.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonocarroll/ggghost/HEAD/README_supp/scooby.gif -------------------------------------------------------------------------------- /README_supp/mycoolplot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonocarroll/ggghost/HEAD/README_supp/mycoolplot.gif -------------------------------------------------------------------------------- /README_supp/mycoolplot.rds: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonocarroll/ggghost/HEAD/README_supp/mycoolplot.rds -------------------------------------------------------------------------------- /README_supp/README-unnamed-chunk-10-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonocarroll/ggghost/HEAD/README_supp/README-unnamed-chunk-10-1.png -------------------------------------------------------------------------------- /README_supp/README-unnamed-chunk-11-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonocarroll/ggghost/HEAD/README_supp/README-unnamed-chunk-11-1.png -------------------------------------------------------------------------------- /README_supp/README-unnamed-chunk-12-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonocarroll/ggghost/HEAD/README_supp/README-unnamed-chunk-12-1.png -------------------------------------------------------------------------------- /README_supp/README-unnamed-chunk-12-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonocarroll/ggghost/HEAD/README_supp/README-unnamed-chunk-12-2.png -------------------------------------------------------------------------------- /README_supp/README-unnamed-chunk-13-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonocarroll/ggghost/HEAD/README_supp/README-unnamed-chunk-13-1.png -------------------------------------------------------------------------------- /README_supp/README-unnamed-chunk-13-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonocarroll/ggghost/HEAD/README_supp/README-unnamed-chunk-13-2.png -------------------------------------------------------------------------------- /README_supp/README-unnamed-chunk-14-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonocarroll/ggghost/HEAD/README_supp/README-unnamed-chunk-14-1.png -------------------------------------------------------------------------------- /README_supp/README-unnamed-chunk-16-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonocarroll/ggghost/HEAD/README_supp/README-unnamed-chunk-16-1.png -------------------------------------------------------------------------------- /README_supp/README-unnamed-chunk-17-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonocarroll/ggghost/HEAD/README_supp/README-unnamed-chunk-17-1.png -------------------------------------------------------------------------------- /README_supp/README-unnamed-chunk-18-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonocarroll/ggghost/HEAD/README_supp/README-unnamed-chunk-18-1.png -------------------------------------------------------------------------------- /README_supp/README-unnamed-chunk-19-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonocarroll/ggghost/HEAD/README_supp/README-unnamed-chunk-19-1.png -------------------------------------------------------------------------------- /README_supp/README-unnamed-chunk-8-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonocarroll/ggghost/HEAD/README_supp/README-unnamed-chunk-8-1.png -------------------------------------------------------------------------------- /README_supp/README-unnamed-chunk-9-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonocarroll/ggghost/HEAD/README_supp/README-unnamed-chunk-9-1.png -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | ## R CMD check results 2 | 3 | Checked with Rhubv2 on linux, m1-san, macos, macos-arm64, windows 4 | Checked on R-devel with win-builder 5 | 6 | 0 errors | 0 warnings | 0 notes 7 | 8 | * This is a minor release to align to the upcoming ggplot2 release 9 | -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^README\.Rmd$ 4 | ^README-.*\.png$ 5 | ^.travis.yml$ 6 | ^appveyor.yml$ 7 | ^cran$ 8 | ^cran-comments\.md$ 9 | ^cran-comments\.html$ 10 | ^cran-comments-1\.md$ 11 | ^\.gitignore$ 12 | ^README_supp$ 13 | ^\.github$ 14 | ^CRAN-SUBMISSION$ 15 | -------------------------------------------------------------------------------- /R/zzz.R: -------------------------------------------------------------------------------- 1 | .onAttach <- function(...) { 2 | AttachNote <- " 3 | Please be aware: this development package has the potential to mess with your ggplot2 calls. 4 | If you find a bug, please let me know: https://github.com/jonocarroll/ggghost/issues 5 | " 6 | packageStartupMessage(paste(strwrap(AttachNote), collapse = "\n")) 7 | } 8 | -------------------------------------------------------------------------------- /man/is.ggghost.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/ghost.R 3 | \name{is.ggghost} 4 | \alias{is.ggghost} 5 | \title{Reports whether x is a ggghost object} 6 | \usage{ 7 | is.ggghost(x) 8 | } 9 | \arguments{ 10 | \item{x}{An object to test} 11 | } 12 | \value{ 13 | logical; \code{TRUE} if \code{x} inherits class \code{ggghost} 14 | } 15 | \description{ 16 | Reports whether x is a ggghost object 17 | } 18 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method("+",gg) 4 | S3method("-",gg) 5 | S3method(print,ggghost) 6 | S3method(subset,ggghost) 7 | S3method(summary,ggghost) 8 | export("%g<%") 9 | export("supp_data<-") 10 | export(is.ggghost) 11 | export(lazarus) 12 | export(reanimate) 13 | export(recover_data) 14 | export(supp_data) 15 | importFrom(animation,ani.options) 16 | importFrom(animation,saveGIF) 17 | importFrom(ggplot2,"%+%") 18 | importFrom(ggplot2,is.ggplot) 19 | importFrom(ggplot2,is.theme) 20 | -------------------------------------------------------------------------------- /man/supp_data.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/ghost.R 3 | \name{supp_data} 4 | \alias{supp_data} 5 | \title{Inspect the supplementary data attached to a ggghost object} 6 | \usage{ 7 | supp_data(x) 8 | } 9 | \arguments{ 10 | \item{x}{A ggghost object} 11 | } 12 | \value{ 13 | A list with two elements: the name of the supplementary data, and the 14 | supplementary data itself 15 | } 16 | \description{ 17 | Inspect the supplementary data attached to a ggghost object 18 | } 19 | -------------------------------------------------------------------------------- /tests/testthat/test_gif.R: -------------------------------------------------------------------------------- 1 | library(ggghost) 2 | library(ggplot2) 3 | context("(Re-)animation") 4 | 5 | dat <- data.frame(x = 1:100, y = rnorm(100)) 6 | z %g<% ggplot(dat, aes(x, y)) 7 | z <- z + geom_point(col = "steelblue") 8 | z <- z + theme_bw() 9 | z <- z + labs(title = "My cool ggplot") 10 | z <- z + labs(x = "x axis", y = "y axis") 11 | z <- z + geom_smooth() 12 | giftest <- lazarus(z, "testgif.gif") 13 | 14 | test_that("gif can be created without errors/warnings", { 15 | expect_identical(giftest, TRUE) 16 | }) 17 | 18 | if (file.exists("testgif.gif")) file.remove("testgif.gif") 19 | -------------------------------------------------------------------------------- /man/print.ggghost.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/ghost.R 3 | \name{print.ggghost} 4 | \alias{print.ggghost} 5 | \title{Collect ggghost calls and produce the ggplot output} 6 | \usage{ 7 | \method{print}{ggghost}(x, ...) 8 | } 9 | \arguments{ 10 | \item{x}{A ggghost object to be made into a ggplot grob} 11 | 12 | \item{...}{Not used, provided for \code{print.default} generic consistency.} 13 | } 14 | \value{ 15 | The ggplot plot data (invisibly). Used for the side-effect of producing a ggplot plot. 16 | } 17 | \description{ 18 | Collect ggghost calls and produce the ggplot output 19 | } 20 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: ggghost 2 | Title: Capture the Spirit of Your 'ggplot2' Calls 3 | Version: 0.2.3 4 | Authors@R: person("Jonathan", "Carroll", email = "rpkg@jcarroll.com.au", role = c("aut", "cre")) 5 | Maintainer: Jonathan Carroll 6 | Description: Creates a reproducible 'ggplot2' object by storing the data and calls. 7 | Depends: 8 | R (>= 3.2.0), 9 | ggplot2, 10 | animation 11 | License: GPL(>=3) 12 | Encoding: UTF-8 13 | URL: https://github.com/jonocarroll/ggghost 14 | BugReports: https://github.com/jonocarroll/ggghost/issues 15 | RoxygenNote: 7.3.2 16 | Suggests: testthat 17 | Roxygen: list(markdown = TRUE) 18 | -------------------------------------------------------------------------------- /man/supp_data-set.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/ghost.R 3 | \name{supp_data<-} 4 | \alias{supp_data<-} 5 | \title{Attach supplementary data to a ggghost object} 6 | \usage{ 7 | supp_data(x) <- value 8 | } 9 | \arguments{ 10 | \item{x}{A ggghost object to which the supplementary data should be 11 | attached} 12 | 13 | \item{value}{Supplementary data to attach to the ggghost object, probably 14 | used as an additional data input to a \code{scale_*} or \code{geom_*} call} 15 | } 16 | \value{ 17 | The original object with \code{suppdata} attribute 18 | } 19 | \description{ 20 | Attach supplementary data to a ggghost object 21 | } 22 | -------------------------------------------------------------------------------- /tests/testthat/test_construct.R: -------------------------------------------------------------------------------- 1 | library(ggghost) 2 | library(ggplot2) 3 | context("Construction") 4 | 5 | dat <- data.frame(x = 1:100, y = rnorm(100)) 6 | ggghostx %g<% ggplot(dat, aes(x, y)) 7 | ggghostx2 %g<% ggplot(aes(x, y), data = dat) 8 | 9 | test_that("%g<% constructs a ggghost object", { 10 | expect_s3_class(ggghostx, "ggghost") 11 | expect_s3_class(ggghostx, "gg") 12 | expect_type(ggghostx[[1]], "language") 13 | expect_true(grepl("ggplot", as.character(ggghostx))) 14 | }) 15 | 16 | test_that("%g<% captures data regardless of where it is in the argument list", { 17 | expect_type(attr(ggghostx2, "data"), "list") 18 | expect_s3_class(attr(ggghostx2, "data")$data, "data.frame") 19 | }) 20 | -------------------------------------------------------------------------------- /man/plus-ggghost.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/ghost.R 3 | \name{+.gg} 4 | \alias{+.gg} 5 | \title{Add a New ggplot Component to a ggghost Object} 6 | \usage{ 7 | \method{+}{gg}(e1, e2) 8 | } 9 | \arguments{ 10 | \item{e1}{An object of class \code{ggghost}} 11 | 12 | \item{e2}{A component to add to \code{e1}} 13 | } 14 | \value{ 15 | Appends the \code{e2} call to the \code{ggghost} structure 16 | } 17 | \description{ 18 | This operator allows you to add objects to a ggghost object in the style of @hrbrmstr. 19 | } 20 | \examples{ 21 | #' ## create a ggghost object 22 | tmpdata <- data.frame(x = 1:100, y = rnorm(100)) 23 | 24 | z \%g<\% ggplot(tmpdata, aes(x,y)) 25 | z <- z + geom_point(col = "steelblue") 26 | z <- z + theme_bw() 27 | z <- z + labs(title = "My cool ggplot") 28 | z <- z + labs(x = "x axis", y = "y axis") 29 | z <- z + geom_smooth() 30 | } 31 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # CHANGES in ggghost 0.2.3 2 | ========== 3 | 4 | * align to new ggplot2 release (changed examples only) 5 | 6 | # CHANGES in ggghost 0.2.1 7 | ========== 8 | 9 | * Minor cleanup, inline ggplot2 params 10 | 11 | 12 | # CHANGES in ggghost 0.2.1 13 | ========== 14 | 15 | * Corrected a minor bug which dropped supplementary data when using `+` or `-`. 16 | 17 | # CHANGES in ggghost 0.2.0 18 | ========== 19 | 20 | * Allowed inclusion of supplementary data, with recovery (closes #3). 21 | 22 | * Added a warning to `recover_data` when data object exists in the calling frame 23 | but has changed since being captured, with opt-out where used interactively 24 | (closes #4). Note that this is responsible for the less than 100% code coverage 25 | (everything except `if (interactive())` is covered. 26 | 27 | * Moved README supplementary objects out of package, reducing package filesize 28 | (closes #2). 29 | 30 | # ggghost 0.1.0 31 | ========== 32 | 33 | * Initial CRAN submission. No ERRORs, WARNINGs, or NOTEs. 34 | -------------------------------------------------------------------------------- /man/grapes-g-less-than-grapes.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/ghost.R 3 | \name{\%g<\%} 4 | \alias{\%g<\%} 5 | \title{Begin constructing a ggghost cache} 6 | \usage{ 7 | lhs \%g<\% rhs 8 | } 9 | \arguments{ 10 | \item{lhs}{LHS of call} 11 | 12 | \item{rhs}{RHS of call} 13 | } 14 | \value{ 15 | Assigns the \code{ggghost} structure to the \code{lhs} symbol. 16 | } 17 | \description{ 18 | The data and initial \code{ggpot()} call are stored as a list (call) with 19 | attribute (data). 20 | } 21 | \details{ 22 | The data must be passed into the \code{ggplot} call directly. 23 | Passing this in via a magrittr pipe remains as a future improvement. The 24 | newly created \code{ggghost} object is a list of length 1 containing the 25 | \code{ggplot} call, with attribute \code{data}; another list, containing 26 | the \code{data_name} and \code{data} itself. 27 | } 28 | \examples{ 29 | ## create a ggghost object 30 | tmpdata <- data.frame(x = 1:100, y = rnorm(100)) 31 | 32 | z \%g<\% ggplot(tmpdata, aes(x,y)) 33 | } 34 | -------------------------------------------------------------------------------- /man/identify_data.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/ghost.R 3 | \name{identify_data} 4 | \alias{identify_data} 5 | \title{Identify the data passed to ggplot} 6 | \usage{ 7 | identify_data( 8 | data, 9 | mapping = ggplot2::aes(), 10 | ..., 11 | environment = parent.frame() 12 | ) 13 | } 14 | \arguments{ 15 | \item{data}{Default dataset to use for plot. If not already a data.frame, 16 | will be converted to one by \code{\link[ggplot2:fortify]{ggplot2::fortify()}}. If not specified, 17 | must be supplied in each layer added to the plot.} 18 | 19 | \item{mapping}{Default list of aesthetic mappings to use for plot. 20 | If not specified, must be supplied in each layer added to the plot.} 21 | 22 | \item{...}{Other arguments passed on to methods. Not currently used.} 23 | 24 | \item{environment}{(deprecated) Used prior to tidy evaluation.} 25 | } 26 | \value{ 27 | Name of the \code{data.frame} passed to \code{ggplot} 28 | } 29 | \description{ 30 | Duplicate arguments to ggplot2::ggplot with the intent that the \code{data} 31 | argument can be captured and identified. 32 | } 33 | \keyword{internal} 34 | -------------------------------------------------------------------------------- /tests/testthat/test_methods.R: -------------------------------------------------------------------------------- 1 | library(ggghost) 2 | library(ggplot2) 3 | context("Methods") 4 | 5 | dat <- data.frame(x = 1:100, y = rnorm(100)) 6 | ggghostx %g<% ggplot(dat, aes(x, y)) 7 | ggghostx <- ggghostx + geom_point(col = "red") 8 | ggghostx <- ggghostx + geom_line(col = "steelblue") 9 | summary1 <- summary(ggghostx) 10 | summary2 <- summary(ggghostx, combine = TRUE) 11 | ggsubset <- subset(ggghostx, c(1, 3)) 12 | summary3 <- summary(ggsubset) 13 | summary4 <- summary(ggsubset, combine = TRUE) 14 | 15 | test_that("ggghost methods behave correctly", { 16 | expect_s3_class(print(ggghostx), "gg") 17 | expect_s3_class(print(ggghostx), "ggplot") 18 | 19 | expect_type(summary1, "list") 20 | expect_identical(length(summary1), 3L) 21 | expect_type(summary2, "character") 22 | expect_true(grepl("ggplot", summary2)) 23 | expect_true(grepl("geom_point", summary2)) 24 | 25 | expect_s3_class(ggsubset, "gg") 26 | expect_s3_class(ggsubset, "ggghost") 27 | 28 | expect_type(summary3, "list") 29 | expect_identical(length(summary3), 2L) 30 | expect_type(summary4, "character") 31 | expect_true(grepl("ggplot", summary4)) 32 | expect_true(grepl("geom_line", summary4)) 33 | expect_false(grepl("geom_point", summary4)) 34 | }) 35 | -------------------------------------------------------------------------------- /man/recover_data.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/ghost.R 3 | \name{recover_data} 4 | \alias{recover_data} 5 | \title{Recover data Stored in a ggghost object} 6 | \usage{ 7 | recover_data(x, supp = TRUE) 8 | } 9 | \arguments{ 10 | \item{x}{A ggghost object from which to extract the data.} 11 | 12 | \item{supp}{(logical) Should the supplementary data be extracted also?} 13 | } 14 | \value{ 15 | A \code{data.frame} of the original data, named as it was when used 16 | in \code{ggplot(data)} 17 | } 18 | \description{ 19 | The data used to generate a plot is an essential requirement for a 20 | reproducible graphic. This is somewhat available from a ggplot \code{grob} 21 | (in raw form) but it it not easily accessible, and isn't named the same way 22 | as the original call. 23 | } 24 | \details{ 25 | This function retrieves the data from the ggghost object as it was when it 26 | was originally called. 27 | 28 | If supplementary data has also been attached using \code{\link{supp_data}} 29 | then this will also be recovered (if requested). 30 | 31 | When used interactively, a warning will be produced if the data to be 32 | extracted exists in the workspace but not identical to the captured version. 33 | } 34 | -------------------------------------------------------------------------------- /man/subset.ggghost.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/ghost.R 3 | \name{subset.ggghost} 4 | \alias{subset.ggghost} 5 | \title{Extract a subset of a ggghost object} 6 | \usage{ 7 | \method{subset}{ggghost}(x, ...) 8 | } 9 | \arguments{ 10 | \item{x}{A ggghost object to subset} 11 | 12 | \item{...}{A logical expression indicating which elements to select. 13 | Typically a vector of list numbers, but potentially a vector of logicals or 14 | logical expressions.} 15 | } 16 | \value{ 17 | Another ggghost object containing only the calls selected. 18 | } 19 | \description{ 20 | Alternative to subtracting calls using \code{-.gg}, this method allows one to 21 | select the desired components of the available calls and have those 22 | evaluated. 23 | } 24 | \examples{ 25 | ## create a ggghost object 26 | tmpdata <- data.frame(x = 1:100, y = rnorm(100)) 27 | 28 | z \%g<\% ggplot(tmpdata, aes(x,y)) 29 | z <- z + geom_point(col = "steelblue") 30 | z <- z + theme_bw() 31 | z <- z + labs(title = "My cool ggplot") 32 | z <- z + labs(x = "x axis", y = "y axis") 33 | z <- z + geom_smooth() 34 | 35 | ## remove the labels and theme 36 | subset(z, c(1,2,6)) 37 | ## or 38 | subset(z, c(TRUE,TRUE,FALSE,FALSE,FALSE,TRUE)) 39 | } 40 | -------------------------------------------------------------------------------- /R/ggghost-package.R: -------------------------------------------------------------------------------- 1 | #' ggghost: Capture the spirit of your ggplot calls 2 | #' 3 | #' Creates a reproducible container for ggplot, storing the data and calls required to produce a plot. 4 | #' 5 | #' `ggplot2` stores the information needed to build the graph as a `grob`, but 6 | #' that's what the **computer** needs to know about in order to build the graph. 7 | #' As humans, we're more interested in what commands were issued in order to 8 | #' build the graph. For good reproducibility, the calls need to be applied to the 9 | #' relevant data. While this is somewhat available by deconstructing the `grob`, 10 | #' it's not the simplest approach. 11 | #' 12 | #' Here is one option that solves that problem. 13 | #' 14 | #' `ggghost` stores the data used in a `ggplot()` call, and collects `ggplot` 15 | #' commands (usually separated by `+`) as they are applied, in effect lazily 16 | #' collecting the calls. Once the object is requested, the `print` method 17 | #' combines the individual calls back into the total plotting command and 18 | #' executes it. This is where the call would usually be discarded. Instead, a 19 | #' "ghost" of the commands lingers in the object for further investigation, 20 | #' subsetting, adding to, or subtracting from. 21 | #' 22 | #' @keywords internal 23 | "_PACKAGE" 24 | 25 | ## usethis namespace: start 26 | ## usethis namespace: end 27 | NULL 28 | -------------------------------------------------------------------------------- /man/summary.ggghost.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/ghost.R 3 | \name{summary.ggghost} 4 | \alias{summary.ggghost} 5 | \title{List the calls contained in a ggghost object} 6 | \usage{ 7 | \method{summary}{ggghost}(object, ...) 8 | } 9 | \arguments{ 10 | \item{object}{A ggghost object to present} 11 | 12 | \item{...}{Mainly provided for \code{summary.default} generic consistency. 13 | When \code{combine} is passed as an argument (arbitrary value) the list of 14 | calls is concatenated into a single string as one might write the ggplot 15 | call.} 16 | } 17 | \value{ 18 | Either a list of ggplot calls or a string of such concatenated with " + " 19 | } 20 | \description{ 21 | Summarises a ggghost object by presenting the contained calls in the order 22 | they were added. Optionally concatenates these into a single ggplot call. 23 | } 24 | \details{ 25 | The data is also included in ggghost objects. If this is also 26 | desired in the output, use \code{str}. See example. 27 | } 28 | \examples{ 29 | ## present the ggghost object as a list 30 | tmpdata <- data.frame(x = 1:100, y = rnorm(100)) 31 | 32 | z \%g<\% ggplot(tmpdata, aes(x,y)) 33 | z <- z + geom_point(col = "steelblue") 34 | summary(z) 35 | 36 | ## present the ggghost object as a string 37 | summary(z, combine = TRUE) # Note, value of 'combine' is arbitrary 38 | 39 | ## to inspect the data structure also captured, use str() 40 | str(z) 41 | } 42 | -------------------------------------------------------------------------------- /man/reanimate.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/ghost.R 3 | \name{reanimate} 4 | \alias{reanimate} 5 | \alias{lazarus} 6 | \title{Bring a ggplot to life (re-animate)} 7 | \usage{ 8 | reanimate( 9 | object, 10 | gifname = "ggghost.gif", 11 | interval = 1, 12 | ani.width = 600, 13 | ani.height = 600 14 | ) 15 | 16 | lazarus( 17 | object, 18 | gifname = "ggghost.gif", 19 | interval = 1, 20 | ani.width = 600, 21 | ani.height = 600 22 | ) 23 | } 24 | \arguments{ 25 | \item{object}{A ggghost object to animate} 26 | 27 | \item{gifname}{Output filename to save the .gif to (not including any path, 28 | will be saved to current directory)} 29 | 30 | \item{interval}{A positive number to set the time interval of the animation 31 | (unit in seconds); see \code{animation::ani.options}} 32 | 33 | \item{ani.width}{width of image frames (unit in px); see 34 | \code{animation::ani.options}} 35 | 36 | \item{ani.height}{height of image frames (unit in px); see 37 | \code{animation::ani.options}} 38 | } 39 | \value{ 40 | \code{TRUE} if it gets that far 41 | } 42 | \description{ 43 | Creates an animation showing the stepwise process of building up a ggplot. 44 | Successively adds calls from a ggghost object and then combines these into an 45 | animated GIF. 46 | } 47 | \examples{ 48 | \dontrun{ 49 | ## create an animation showing the process of building up a plot 50 | reanimate(z, "mycoolplot.gif") 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/testthat/test_Ops.R: -------------------------------------------------------------------------------- 1 | library(ggghost) 2 | context("Ops (+, -)") 3 | 4 | dat <- data.frame(x = 1:100, y = rnorm(100)) 5 | gg1 <- ggplot2::ggplot(dat, aes(x, y)) 6 | gg2 <- ggplot2::geom_point() 7 | ggx <- gg1 + gg2 8 | 9 | test_that("+ behaves as normal for ordinary objects", { 10 | expect_equal(2L + 3L, 5L) 11 | expect_equal(2. + 3., 5.) 12 | expect_equal(c(2L, 3.) + c(1L, 1.), c(3., 4.)) 13 | expect_error("a" + "b") 14 | expect_s3_class(ggx, "gg") 15 | }) 16 | 17 | test_that("- behaves as normal for ordinary objects", { 18 | expect_equal(3L - 2L, 1L) 19 | expect_equal(3. - 2., 1.) 20 | expect_equal(c(2L, 3.) - c(1L, 1.), c(1., 2.)) 21 | expect_error("a" - "b") 22 | }) 23 | 24 | ggghostx %g<% ggplot2::ggplot(dat, aes(x, y)) 25 | ggghostx <- ggghostx + ggplot2::geom_point(col = "red") 26 | ggghostx2 <- ggghostx - geom_point() 27 | 28 | test_that("+ produces new behaviour", { 29 | expect_s3_class(ggghostx, "ggghost") 30 | expect_s3_class(eval(ggghostx), "gg") 31 | expect_equal(length(ggghostx), 2) 32 | expect_true(grepl("geom_point", summary(ggghostx, combine = TRUE))) 33 | }) 34 | 35 | test_that("- produces new behaviour", { 36 | expect_s3_class(ggghostx2, "ggghost") 37 | expect_s3_class(eval(ggghostx2), "gg") 38 | expect_equal(length(ggghostx2), 1) 39 | expect_false(grepl("geom_point", summary(ggghostx2, combine = TRUE))) 40 | }) 41 | 42 | test_that("- fails if trying to remove ggplot() or missing call", { 43 | expect_warning(ggghostx - ggplot(), "ggghostbuster") 44 | expect_warning(ggghostx - geom_bar(), "ggghostbuster") 45 | }) 46 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | 8 | name: R-CMD-check.yaml 9 | 10 | permissions: read-all 11 | 12 | jobs: 13 | R-CMD-check: 14 | runs-on: ${{ matrix.config.os }} 15 | 16 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 17 | 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | config: 22 | - {os: macos-latest, r: 'release'} 23 | - {os: windows-latest, r: 'release'} 24 | - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} 25 | - {os: ubuntu-latest, r: 'release'} 26 | - {os: ubuntu-latest, r: 'oldrel-1'} 27 | 28 | env: 29 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 30 | R_KEEP_PKG_SOURCE: yes 31 | 32 | steps: 33 | - uses: actions/checkout@v4 34 | 35 | - uses: r-lib/actions/setup-pandoc@v2 36 | 37 | - uses: r-lib/actions/setup-r@v2 38 | with: 39 | r-version: ${{ matrix.config.r }} 40 | http-user-agent: ${{ matrix.config.http-user-agent }} 41 | use-public-rspm: true 42 | 43 | - uses: r-lib/actions/setup-r-dependencies@v2 44 | with: 45 | extra-packages: any::rcmdcheck 46 | needs: check 47 | 48 | - uses: r-lib/actions/check-r-package@v2 49 | with: 50 | upload-snapshots: true 51 | build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")' 52 | -------------------------------------------------------------------------------- /man/ggghost-package.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/ggghost-package.R 3 | \docType{package} 4 | \name{ggghost-package} 5 | \alias{ggghost} 6 | \alias{ggghost-package} 7 | \title{ggghost: Capture the spirit of your ggplot calls} 8 | \description{ 9 | Creates a reproducible container for ggplot, storing the data and calls required to produce a plot. 10 | } 11 | \details{ 12 | \code{ggplot2} stores the information needed to build the graph as a \code{grob}, but 13 | that's what the \strong{computer} needs to know about in order to build the graph. 14 | As humans, we're more interested in what commands were issued in order to 15 | build the graph. For good reproducibility, the calls need to be applied to the 16 | relevant data. While this is somewhat available by deconstructing the \code{grob}, 17 | it's not the simplest approach. 18 | 19 | Here is one option that solves that problem. 20 | 21 | \code{ggghost} stores the data used in a \code{ggplot()} call, and collects \code{ggplot} 22 | commands (usually separated by \code{+}) as they are applied, in effect lazily 23 | collecting the calls. Once the object is requested, the \code{print} method 24 | combines the individual calls back into the total plotting command and 25 | executes it. This is where the call would usually be discarded. Instead, a 26 | "ghost" of the commands lingers in the object for further investigation, 27 | subsetting, adding to, or subtracting from. 28 | } 29 | \seealso{ 30 | Useful links: 31 | \itemize{ 32 | \item \url{https://github.com/jonocarroll/ggghost} 33 | \item Report bugs at \url{https://github.com/jonocarroll/ggghost/issues} 34 | } 35 | 36 | } 37 | \author{ 38 | \strong{Maintainer}: Jonathan Carroll \email{rpkg@jcarroll.com.au} 39 | 40 | } 41 | \keyword{internal} 42 | -------------------------------------------------------------------------------- /man/minus-ggghost.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/ghost.R 3 | \name{-.gg} 4 | \alias{-.gg} 5 | \title{Remove a call from a ggghost object} 6 | \usage{ 7 | \method{-}{gg}(e1, e2) 8 | } 9 | \arguments{ 10 | \item{e1}{An object of class \code{ggghost}} 11 | 12 | \item{e2}{A component to remove from \code{e1} as either a string or a 13 | language object} 14 | } 15 | \value{ 16 | A \code{ggghost} structure with calls (text) matching \code{e2} 17 | removed, otherwise the same as \code{e1} 18 | } 19 | \description{ 20 | Calls can be removed from the \code{ggghost} object via regex matching of the 21 | function name. All matching calls will be removed based on the match to the 22 | string up to the first bracket, so any arguments are irrelevant. 23 | } 24 | \details{ 25 | For example, subtracting \code{geom_line()} will remove all calls matching 26 | \code{geom_line} regardless of their arguments. 27 | 28 | \code{labs()} has been identified as a special case, as it requires an argument in 29 | order to be recognised as a valid function. Thus, trying to remove it with an 30 | empty argument will fail. That said, the argument doesn't need to match, so 31 | it can be populated with a dummy string or anything that evaluates in scope. 32 | See examples. 33 | } 34 | \examples{ 35 | ## create a ggghost object 36 | tmpdata <- data.frame(x = 1:100, y = rnorm(100)) 37 | 38 | z \%g<\% ggplot(tmpdata, aes(x,y)) 39 | z <- z + geom_point(col = "steelblue") 40 | z <- z + theme_bw() 41 | z <- z + labs(title = "My cool ggplot") 42 | z <- z + labs(x = "x axis", y = "y axis") 43 | z <- z + geom_smooth() 44 | 45 | z - "labs" # removes all labs 46 | z - "title" # removes just the title 47 | z - "axis" # removes the axis labels 48 | z - geom_point() # removes points 49 | z - theme_bw() # removes theme_bw() 50 | } 51 | -------------------------------------------------------------------------------- /tests/testthat/test_data.R: -------------------------------------------------------------------------------- 1 | library(ggghost) 2 | library(ggplot2) 3 | context("Data recovery") 4 | 5 | dat <- df_saved <- data.frame(x = 1:100, y = rnorm(100)) 6 | sdat <- sdat_saved <- data.frame(a = 21:30, y = rnorm(10)) 7 | ggghostx %g<% ggplot(dat, aes(x, y)) 8 | ggghostx <- ggghostx + geom_point(col = "red") 9 | rm(dat) 10 | recover_data(ggghostx) 11 | 12 | test_that("data can be successfully recovered", { 13 | expect_identical(df_saved, dat) 14 | }) 15 | 16 | dat <- data.frame(x = 1:100, y = rnorm(100)) 17 | 18 | test_that("overwriting changed data produces a warning", { 19 | expect_warning(recover_data(ggghostx, supp = FALSE)) 20 | }) 21 | 22 | test_that("supplementary data is rejected from a non-ggghost object", { 23 | expect_error(supp_data(sdat) <- c(1, 2)) 24 | }) 25 | 26 | test_that("non-existant supplementary data cannot be extracted from a ggghost object", { 27 | expect_warning(recover_data(ggghostx, supp = TRUE)) 28 | }) 29 | 30 | test_that("supplementary data can be added to a ggghost object", { 31 | expect_silent(supp_data(ggghostx) <- sdat) 32 | }) 33 | 34 | supp_data(ggghostx) <- sdat 35 | 36 | test_that("adding additional supplementary data produces a warning", { 37 | expect_warning(supp_data(ggghostx) <- sdat) 38 | }) 39 | 40 | 41 | test_that("supplementary data be inspected", { 42 | expect_type(supp_data(ggghostx), "list") 43 | expect_s3_class(supp_data(ggghostx)[[2]], "data.frame") 44 | expect_identical(supp_data(ggghostx)[[2]], sdat) 45 | }) 46 | 47 | sdat <- c(1, 2) 48 | 49 | test_that("overwriting changed supplementary data produces a warning", { 50 | expect_warning(recover_data(ggghostx, supp = TRUE)) 51 | }) 52 | 53 | recover_data(ggghostx, supp = TRUE) 54 | 55 | test_that("supplementary data be successfully recovered", { 56 | expect_identical(sdat_saved, sdat) 57 | }) 58 | 59 | ggghostx <- ggghostx + geom_line() 60 | 61 | test_that("supplementary data remains after adding a call", { 62 | expect_identical(supp_data(ggghostx)[[2]], sdat) 63 | }) 64 | -------------------------------------------------------------------------------- /.github/workflows/rhub.yaml: -------------------------------------------------------------------------------- 1 | # R-hub's generic GitHub Actions workflow file. It's canonical location is at 2 | # https://github.com/r-hub/actions/blob/v1/workflows/rhub.yaml 3 | # You can update this file to a newer version using the rhub2 package: 4 | # 5 | # rhub::rhub_setup() 6 | # 7 | # It is unlikely that you need to modify this file manually. 8 | 9 | name: R-hub 10 | run-name: "${{ github.event.inputs.id }}: ${{ github.event.inputs.name || format('Manually run by {0}', github.triggering_actor) }}" 11 | 12 | on: 13 | workflow_dispatch: 14 | inputs: 15 | config: 16 | description: 'A comma separated list of R-hub platforms to use.' 17 | type: string 18 | default: 'linux,windows,macos' 19 | name: 20 | description: 'Run name. You can leave this empty now.' 21 | type: string 22 | id: 23 | description: 'Unique ID. You can leave this empty now.' 24 | type: string 25 | 26 | jobs: 27 | 28 | setup: 29 | runs-on: ubuntu-latest 30 | outputs: 31 | containers: ${{ steps.rhub-setup.outputs.containers }} 32 | platforms: ${{ steps.rhub-setup.outputs.platforms }} 33 | 34 | steps: 35 | # NO NEED TO CHECKOUT HERE 36 | - uses: r-hub/actions/setup@v1 37 | with: 38 | config: ${{ github.event.inputs.config }} 39 | id: rhub-setup 40 | 41 | linux-containers: 42 | needs: setup 43 | if: ${{ needs.setup.outputs.containers != '[]' }} 44 | runs-on: ubuntu-latest 45 | name: ${{ matrix.config.label }} 46 | strategy: 47 | fail-fast: false 48 | matrix: 49 | config: ${{ fromJson(needs.setup.outputs.containers) }} 50 | container: 51 | image: ${{ matrix.config.container }} 52 | 53 | steps: 54 | - uses: r-hub/actions/checkout@v1 55 | - uses: r-hub/actions/platform-info@v1 56 | with: 57 | token: ${{ secrets.RHUB_TOKEN }} 58 | job-config: ${{ matrix.config.job-config }} 59 | - uses: r-hub/actions/setup-deps@v1 60 | with: 61 | token: ${{ secrets.RHUB_TOKEN }} 62 | job-config: ${{ matrix.config.job-config }} 63 | - uses: r-hub/actions/run-check@v1 64 | with: 65 | token: ${{ secrets.RHUB_TOKEN }} 66 | job-config: ${{ matrix.config.job-config }} 67 | 68 | other-platforms: 69 | needs: setup 70 | if: ${{ needs.setup.outputs.platforms != '[]' }} 71 | runs-on: ${{ matrix.config.os }} 72 | name: ${{ matrix.config.label }} 73 | strategy: 74 | fail-fast: false 75 | matrix: 76 | config: ${{ fromJson(needs.setup.outputs.platforms) }} 77 | 78 | steps: 79 | - uses: r-hub/actions/checkout@v1 80 | - uses: r-hub/actions/setup-r@v1 81 | with: 82 | job-config: ${{ matrix.config.job-config }} 83 | token: ${{ secrets.RHUB_TOKEN }} 84 | - uses: r-hub/actions/platform-info@v1 85 | with: 86 | token: ${{ secrets.RHUB_TOKEN }} 87 | job-config: ${{ matrix.config.job-config }} 88 | - uses: r-hub/actions/setup-deps@v1 89 | with: 90 | job-config: ${{ matrix.config.job-config }} 91 | token: ${{ secrets.RHUB_TOKEN }} 92 | - uses: r-hub/actions/run-check@v1 93 | with: 94 | job-config: ${{ matrix.config.job-config }} 95 | token: ${{ secrets.RHUB_TOKEN }} 96 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | editor_options: 4 | chunk_output_type: console 5 | --- 6 | 7 | 8 | [![R-CMD-check](https://github.com/jonocarroll/ggghost/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/jonocarroll/ggghost/actions/workflows/R-CMD-check.yaml) 9 | 10 | 11 | 12 | 13 | ```{r, echo = FALSE} 14 | knitr::opts_chunk$set( 15 | collapse = TRUE, 16 | message = FALSE, 17 | warning = FALSE, 18 | comment = "#>", 19 | fig.path = "README_supp/README-" 20 | ) 21 | ``` 22 | 23 | # :ghost: _Oh, no! I think I saw a ... g-g-ghost_ 24 | 25 | ![](https://github.com/jonocarroll/ggghost/raw/master/README_supp/scooby.gif) 26 | 27 | Capture the spirit of your `ggplot2` calls. 28 | 29 | ## Motivation 30 | 31 | `ggplot2::ggplot()` stores the information needed to build the graph as a `grob`, but that's what the **computer** needs to know about in order to build the graph. As humans, we're more interested in what commands were issued in order to build the graph. For good reproducibility, the calls need to be applied to the relevant data. While this is somewhat available by deconstructing the `grob`, it's not the simplest approach. 32 | 33 | Here is one option that solves that problem. 34 | 35 | `ggghost` stores the data used in a `ggplot()` call, and collects `ggplot2` commands (usually separated by `+`) as they are applied, in effect lazily collecting the calls. Once the object is requested, the `print` method combines the individual calls back into the total plotting command and executes it. This is where the call would usually be discarded. Instead, a "ghost" of the commands lingers in the object for further investigation, subsetting, adding to, or subtracting from. 36 | 37 | ## Installation 38 | 39 | You can install `ggghost` from CRAN with: 40 | 41 | ```{r, eval=FALSE} 42 | install.packages("ggghost") 43 | ``` 44 | or the development version from github with: 45 | 46 | ```{r, eval=FALSE} 47 | # install.packages("devtools") 48 | devtools::install_github("jonocarroll/ggghost") 49 | ``` 50 | 51 | ## Usage 52 | 53 | use `%g<%` to initiate storage of the `ggplot2` calls then add to the call with each logical call on a new line (@hrbrmstr style) 54 | 55 | ```{r} 56 | tmpdata <- data.frame(x = 1:100, y = rnorm(100)) 57 | head(tmpdata) 58 | ``` 59 | 60 | ```{r, results='hide'} 61 | library(ggplot2) 62 | library(ggghost) 63 | z %g<% ggplot(tmpdata, aes(x, y)) 64 | z <- z + geom_point(col = "steelblue") 65 | z <- z + theme_bw() 66 | z <- z + labs(title = "My cool ggplot") 67 | z <- z + labs(x = "x axis", y = "y axis") 68 | z <- z + geom_smooth() 69 | ``` 70 | 71 | This invisibly stores the `ggplot2` calls in a list which can be reviewed either with the list of calls 72 | ```{r} 73 | summary(z) 74 | ``` 75 | 76 | or the concatenated call 77 | ```{r} 78 | summary(z, combine = TRUE) 79 | ``` 80 | 81 | The plot can be generated using a `print` method 82 | ```{r} 83 | z 84 | ``` 85 | 86 | which re-evaluates the list of calls and applies them to the saved data, meaning that the plot remains reproducible even if the data source is changed/destroyed. 87 | 88 | The call list can be subset, removing parts of the call 89 | ```{r} 90 | subset(z, c(1,2,6)) 91 | ``` 92 | 93 | Plot features can be removed by name, a task that would otherwise have involved re-generating the entire plot 94 | ```{r} 95 | z2 <- z + geom_line(col = "coral") 96 | z2 - geom_point() 97 | ``` 98 | 99 | Calls are removed based on matching to the regex `\\(.*$` (from the first 100 | bracket to the end of the call), so arguments are irrelevant. The possible 101 | matches can be found with `summary(z)` as above 102 | 103 | 104 | The object still generates all the `grob` info, it's just stored as calls rather than a completed image. 105 | ```{r, fig.show='hide'} 106 | str(print(z)) 107 | #> [... truncated ...] 108 | ``` 109 | 110 | Since the `grob` info is still produced, normal `ggplot2` operators can be applied *after* the `print` statement, such as replacing the data 111 | ```{r} 112 | xvals <- seq(0,2*pi,0.1) 113 | tmpdata_new <- data.frame(x = xvals, y = sin(xvals)) 114 | print(z - geom_smooth()) %+% tmpdata_new 115 | ``` 116 | 117 | `ggplot2` calls still work as normal if you want to avoid storing the calls. 118 | ```{r} 119 | ggplot(tmpdata) + geom_point(aes(x,y), col = "red") 120 | ``` 121 | 122 | Since the object is a list, we can stepwise show the process of building up the plot as a (re-)animation 123 | ```{r, eval = FALSE} 124 | lazarus(z, "mycoolplot.gif") 125 | ``` 126 | 127 | ```{r, echo = FALSE} 128 | knitr::include_graphics("README_supp/mycoolplot.gif") 129 | ``` 130 | 131 | A supplementary data object (e.g. for use in a `geom_*` or `scale_*` call) can be added to the `ggghost` object 132 | ```{r} 133 | myColors <- c("alpha" = "red", "beta" = "blue", "gamma" = "green") 134 | supp_data(z) <- myColors 135 | ``` 136 | 137 | These will be recovered along with the primary data. 138 | 139 | For full reproducibility, the entire structure can be saved to an object for re-loading at a later point. This may not have made much sense for a `ggplot2` object, but now both the original data and the calls to generate the plot are saved. Should the environment that generated the plot be destroyed, all is not lost. 140 | ```{r} 141 | saveRDS(z, file = "README_supp/mycoolplot.rds") 142 | rm(z) 143 | rm(tmpdata) 144 | rm(myColors) 145 | exists("z") 146 | exists("tmpdata") 147 | exists("myColors") 148 | ``` 149 | 150 | Reading the `ggghost` object back to the session, both the relevant data and plot-generating calls can be re-executed. 151 | ```{r} 152 | z <- readRDS("README_supp/mycoolplot.rds") 153 | str(z) 154 | 155 | recover_data(z, supp = TRUE) 156 | head(tmpdata) 157 | 158 | myColors 159 | 160 | z 161 | ``` 162 | 163 | We now have a proper reproducible graphic. 164 | 165 | ## Caveats 166 | 167 | * The data _must_ be used as an argument in the `ggplot2` call, not piped in to it. Pipelines such as `z %g<% tmpdata %>% ggplot()` won't work... yet. 168 | * ~~Only one original data set will be stored; the one in the original `ggplot(data = x)` call. If you require supplementary data for some `geom` then you need manage storage/consistency of that.~~ (fixed) 169 | * ~~For removing `labs` calls, an argument _must_ be present. It doesn't need to be the actual one (all will be removed) but it must evaluate in scope. `TRUE` will do fine.~~ 170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | [![R-CMD-check](https://github.com/jonocarroll/ggghost/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/jonocarroll/ggghost/actions/workflows/R-CMD-check.yaml) 5 | 6 | 7 | 8 | 9 | # :ghost: *Oh, no! I think I saw a … g-g-ghost* 10 | 11 | ![](https://github.com/jonocarroll/ggghost/raw/master/README_supp/scooby.gif) 12 | 13 | Capture the spirit of your `ggplot2` calls. 14 | 15 | ## Motivation 16 | 17 | `ggplot2::ggplot()` stores the information needed to build the graph as 18 | a `grob`, but that’s what the **computer** needs to know about in order 19 | to build the graph. As humans, we’re more interested in what commands 20 | were issued in order to build the graph. For good reproducibility, the 21 | calls need to be applied to the relevant data. While this is somewhat 22 | available by deconstructing the `grob`, it’s not the simplest approach. 23 | 24 | Here is one option that solves that problem. 25 | 26 | `ggghost` stores the data used in a `ggplot()` call, and collects 27 | `ggplot2` commands (usually separated by `+`) as they are applied, in 28 | effect lazily collecting the calls. Once the object is requested, the 29 | `print` method combines the individual calls back into the total 30 | plotting command and executes it. This is where the call would usually 31 | be discarded. Instead, a “ghost” of the commands lingers in the object 32 | for further investigation, subsetting, adding to, or subtracting from. 33 | 34 | ## Installation 35 | 36 | You can install `ggghost` from CRAN with: 37 | 38 | ``` r 39 | install.packages("ggghost") 40 | ``` 41 | 42 | or the development version from github with: 43 | 44 | ``` r 45 | # install.packages("devtools") 46 | devtools::install_github("jonocarroll/ggghost") 47 | ``` 48 | 49 | ## Usage 50 | 51 | use `%g<%` to initiate storage of the `ggplot2` calls then add to the 52 | call with each logical call on a new line (@hrbrmstr style) 53 | 54 | ``` r 55 | tmpdata <- data.frame(x = 1:100, y = rnorm(100)) 56 | head(tmpdata) 57 | #> x y 58 | #> 1 1 -0.4418719 59 | #> 2 2 -1.0635266 60 | #> 3 3 -0.2451387 61 | #> 4 4 1.3193699 62 | #> 5 5 -0.6082226 63 | #> 6 6 -0.3583586 64 | ``` 65 | 66 | ``` r 67 | library(ggplot2) 68 | library(ggghost) 69 | z %g<% ggplot(tmpdata, aes(x, y)) 70 | z <- z + geom_point(col = "steelblue") 71 | z <- z + theme_bw() 72 | z <- z + labs(title = "My cool ggplot") 73 | z <- z + labs(x = "x axis", y = "y axis") 74 | z <- z + geom_smooth() 75 | ``` 76 | 77 | This invisibly stores the `ggplot2` calls in a list which can be 78 | reviewed either with the list of calls 79 | 80 | ``` r 81 | summary(z) 82 | #> [[1]] 83 | #> ggplot(tmpdata, aes(x, y)) 84 | #> 85 | #> [[2]] 86 | #> geom_point(col = "steelblue") 87 | #> 88 | #> [[3]] 89 | #> theme_bw() 90 | #> 91 | #> [[4]] 92 | #> labs(title = "My cool ggplot") 93 | #> 94 | #> [[5]] 95 | #> labs(x = "x axis", y = "y axis") 96 | #> 97 | #> [[6]] 98 | #> geom_smooth() 99 | ``` 100 | 101 | or the concatenated call 102 | 103 | ``` r 104 | summary(z, combine = TRUE) 105 | #> [1] "ggplot(tmpdata, aes(x, y)) + geom_point(col = \"steelblue\") + theme_bw() + labs(title = \"My cool ggplot\") + labs(x = \"x axis\", y = \"y axis\") + geom_smooth()" 106 | ``` 107 | 108 | The plot can be generated using a `print` method 109 | 110 | ``` r 111 | z 112 | ``` 113 | 114 | ![](README_supp/README-unnamed-chunk-8-1.png) 115 | 116 | which re-evaluates the list of calls and applies them to the saved data, 117 | meaning that the plot remains reproducible even if the data source is 118 | changed/destroyed. 119 | 120 | The call list can be subset, removing parts of the call 121 | 122 | ``` r 123 | subset(z, c(1,2,6)) 124 | ``` 125 | 126 | ![](README_supp/README-unnamed-chunk-9-1.png) 127 | 128 | Plot features can be removed by name, a task that would otherwise have 129 | involved re-generating the entire plot 130 | 131 | ``` r 132 | z2 <- z + geom_line(col = "coral") 133 | z2 - geom_point() 134 | ``` 135 | 136 | ![](README_supp/README-unnamed-chunk-10-1.png) 137 | 138 | Calls are removed based on matching to the regex `\\(.*$` (from the 139 | first bracket to the end of the call), so arguments are irrelevant. The 140 | possible matches can be found with `summary(z)` as above 141 | 142 | The object still generates all the `grob` info, it’s just stored as 143 | calls rather than a completed image. 144 | 145 | ``` r 146 | str(print(z)) 147 | #> 148 | #> @ data :'data.frame': 100 obs. of 2 variables: 149 | #> .. $ x: int 1 2 3 4 5 6 7 8 9 10 ... 150 | #> .. $ y: num -0.442 -1.064 -0.245 1.319 -0.608 ... 151 | #> @ layers :List of 2 152 | #> .. $ geom_point :Classes 'LayerInstance', 'Layer', 'ggproto', 'gg' 153 | #> [... truncated ...] 154 | ``` 155 | 156 | Since the `grob` info is still produced, normal `ggplot2` operators can 157 | be applied *after* the `print` statement, such as replacing the data 158 | 159 | ``` r 160 | xvals <- seq(0,2*pi,0.1) 161 | tmpdata_new <- data.frame(x = xvals, y = sin(xvals)) 162 | print(z - geom_smooth()) %+% tmpdata_new 163 | ``` 164 | 165 | ![](README_supp/README-unnamed-chunk-12-1.png)![](README_supp/README-unnamed-chunk-12-2.png) 166 | 167 | `ggplot2` calls still work as normal if you want to avoid storing the 168 | calls. 169 | 170 | ``` r 171 | ggplot(tmpdata) + geom_point(aes(x,y), col = "red") 172 | ``` 173 | 174 | ![](README_supp/README-unnamed-chunk-13-1.png) 175 | 176 | Since the object is a list, we can stepwise show the process of building 177 | up the plot as a (re-)animation 178 | 179 | ``` r 180 | lazarus(z, "mycoolplot.gif") 181 | ``` 182 | 183 | ![](README_supp/mycoolplot.gif) 184 | 185 | A supplementary data object (e.g. for use in a `geom_*` or `scale_*` 186 | call) can be added to the `ggghost` object 187 | 188 | ``` r 189 | myColors <- c("alpha" = "red", "beta" = "blue", "gamma" = "green") 190 | supp_data(z) <- myColors 191 | ``` 192 | 193 | These will be recovered along with the primary data. 194 | 195 | For full reproducibility, the entire structure can be saved to an object 196 | for re-loading at a later point. This may not have made much sense for a 197 | `ggplot2` object, but now both the original data and the calls to 198 | generate the plot are saved. Should the environment that generated the 199 | plot be destroyed, all is not lost. 200 | 201 | ``` r 202 | saveRDS(z, file = "README_supp/mycoolplot.rds") 203 | rm(z) 204 | rm(tmpdata) 205 | rm(myColors) 206 | exists("z") 207 | #> [1] FALSE 208 | exists("tmpdata") 209 | #> [1] FALSE 210 | exists("myColors") 211 | #> [1] FALSE 212 | ``` 213 | 214 | Reading the `ggghost` object back to the session, both the relevant data 215 | and plot-generating calls can be re-executed. 216 | 217 | ``` r 218 | z <- readRDS("README_supp/mycoolplot.rds") 219 | str(z) 220 | #> List of 6 221 | #> $ : language ggplot(tmpdata, aes(x, y)) 222 | #> $ : language geom_point(col = "steelblue") 223 | #> $ : language theme_bw() 224 | #> $ : language labs(title = "My cool ggplot") 225 | #> $ : language labs(x = "x axis", y = "y axis") 226 | #> $ : language geom_smooth() 227 | #> - attr(*, "class")= chr [1:2] "ggghost" "gg" 228 | #> - attr(*, "data")=List of 2 229 | #> ..$ data_name: chr "tmpdata" 230 | #> ..$ data :'data.frame': 100 obs. of 2 variables: 231 | #> .. ..$ x: int [1:100] 1 2 3 4 5 6 7 8 9 10 ... 232 | #> .. ..$ y: num [1:100] -0.442 -1.064 -0.245 1.319 -0.608 ... 233 | #> - attr(*, "suppdata")=List of 2 234 | #> ..$ supp_data_name: chr "myColors" 235 | #> ..$ supp_data : Named chr [1:3] "red" "blue" "green" 236 | #> .. ..- attr(*, "names")= chr [1:3] "alpha" "beta" "gamma" 237 | 238 | recover_data(z, supp = TRUE) 239 | head(tmpdata) 240 | #> x y 241 | #> 1 1 -0.4418719 242 | #> 2 2 -1.0635266 243 | #> 3 3 -0.2451387 244 | #> 4 4 1.3193699 245 | #> 5 5 -0.6082226 246 | #> 6 6 -0.3583586 247 | 248 | myColors 249 | #> alpha beta gamma 250 | #> "red" "blue" "green" 251 | 252 | z 253 | ``` 254 | 255 | ![](README_supp/README-unnamed-chunk-18-1.png) 256 | 257 | We now have a proper reproducible graphic. 258 | 259 | ## Caveats 260 | 261 | - The data *must* be used as an argument in the `ggplot2` call, not 262 | piped in to it. Pipelines such as `z %g<% tmpdata %>% ggplot()` won’t 263 | work… yet. 264 | - ~~Only one original data set will be stored; the one in the original 265 | `ggplot(data = x)` call. If you require supplementary data for some 266 | `geom` then you need manage storage/consistency of that.~~ (fixed) 267 | - ~~For removing `labs` calls, an argument *must* be present. It doesn’t 268 | need to be the actual one (all will be removed) but it must evaluate 269 | in scope. `TRUE` will do fine.~~ 270 | -------------------------------------------------------------------------------- /R/ghost.R: -------------------------------------------------------------------------------- 1 | #' Begin constructing a ggghost cache 2 | #' 3 | #' The data and initial \code{ggpot()} call are stored as a list (call) with 4 | #' attribute (data). 5 | #' 6 | #' @details The data must be passed into the \code{ggplot} call directly. 7 | #' Passing this in via a magrittr pipe remains as a future improvement. The 8 | #' newly created \code{ggghost} object is a list of length 1 containing the 9 | #' \code{ggplot} call, with attribute \code{data}; another list, containing 10 | #' the \code{data_name} and \code{data} itself. 11 | #' 12 | #' @param lhs LHS of call 13 | #' @param rhs RHS of call 14 | #' 15 | #' @return Assigns the \code{ggghost} structure to the \code{lhs} symbol. 16 | #' 17 | #' @export 18 | #' @examples 19 | #' ## create a ggghost object 20 | #' tmpdata <- data.frame(x = 1:100, y = rnorm(100)) 21 | #' 22 | #' z %g<% ggplot(tmpdata, aes(x,y)) 23 | `%g<%` <- function(lhs, rhs) { 24 | match <- match.call() 25 | match_lhs <- match[[2]] 26 | match_rhs <- match[[3]] 27 | 28 | parent <- parent.frame() 29 | 30 | new_obj <- structure(list(as.call(match_rhs)), class = c("ggghost", "gg")) 31 | data_name <- eval(parse( 32 | text = sub("ggplot[^(]*", "identify_data", deparse(summary(new_obj)[[1]])) 33 | )) 34 | attr(new_obj, "data") <- list( 35 | data_name = data_name, 36 | data = get(data_name, envir = parent) 37 | ) 38 | 39 | assign(as.character(match_lhs), new_obj, envir = parent) 40 | 41 | return(invisible(NULL)) 42 | } 43 | 44 | 45 | #' Identify the data passed to ggplot 46 | #' 47 | #' Duplicate arguments to ggplot2::ggplot with the intent that the \code{data} 48 | #' argument can be captured and identified. 49 | #' 50 | #' @param data Default dataset to use for plot. If not already a data.frame, 51 | #' will be converted to one by [ggplot2::fortify()]. If not specified, 52 | #' must be supplied in each layer added to the plot. 53 | #' @param mapping Default list of aesthetic mappings to use for plot. 54 | #' If not specified, must be supplied in each layer added to the plot. 55 | #' @param ... Other arguments passed on to methods. Not currently used. 56 | #' @param environment (deprecated) Used prior to tidy evaluation. 57 | #' 58 | #' @return Name of the \code{data.frame} passed to \code{ggplot} 59 | #' 60 | #' @keywords internal 61 | identify_data <- function( 62 | data, 63 | mapping = ggplot2::aes(), 64 | ..., 65 | environment = parent.frame() 66 | ) { 67 | match <- match.call() 68 | data_name <- match[["data"]] 69 | if (is.null(data_name)) stop("could not identify data from call.") 70 | return(as.character(data_name)) 71 | } 72 | 73 | 74 | #' Reports whether x is a ggghost object 75 | #' 76 | #' @param x An object to test 77 | #' 78 | #' @return logical; \code{TRUE} if \code{x} inherits class \code{ggghost} 79 | #' @export 80 | is.ggghost <- function(x) inherits(x, "ggghost") 81 | 82 | # @TODO check compatibility with dev ggplot2 83 | # @BODY urgent, since ggplot2 uodate will be released soon. 84 | #' Add a New ggplot Component to a ggghost Object 85 | #' 86 | #' This operator allows you to add objects to a ggghost object in the style of @hrbrmstr. 87 | #' 88 | #' @param e1 An object of class \code{ggghost} 89 | #' @param e2 A component to add to \code{e1} 90 | #' 91 | #' @return Appends the \code{e2} call to the \code{ggghost} structure 92 | #' @rdname plus-ggghost 93 | #' 94 | #' @importFrom ggplot2 is.theme is.ggplot %+% 95 | #' @export 96 | #' 97 | #' @examples 98 | #' #' ## create a ggghost object 99 | #' tmpdata <- data.frame(x = 1:100, y = rnorm(100)) 100 | #' 101 | #' z %g<% ggplot(tmpdata, aes(x,y)) 102 | #' z <- z + geom_point(col = "steelblue") 103 | #' z <- z + theme_bw() 104 | #' z <- z + labs(title = "My cool ggplot") 105 | #' z <- z + labs(x = "x axis", y = "y axis") 106 | #' z <- z + geom_smooth() 107 | "+.gg" <- function(e1, e2) { 108 | if (is.ggghost(e1)) { 109 | new_obj <- structure( 110 | append(e1, match.call()[[3]]), 111 | class = c("ggghost", "gg") 112 | ) 113 | attr(new_obj, "data") <- attr(e1, "data") 114 | if (!is.null(attr(e1, "suppdata"))) { 115 | attr(new_obj, "suppdata") <- attr(e1, "suppdata") 116 | } 117 | return(new_obj) 118 | } else { 119 | return(e1 %+% e2) 120 | } 121 | } 122 | 123 | 124 | #' Remove a call from a ggghost object 125 | #' 126 | #' Calls can be removed from the \code{ggghost} object via regex matching of the 127 | #' function name. All matching calls will be removed based on the match to the 128 | #' string up to the first bracket, so any arguments are irrelevant. 129 | #' 130 | #' For example, subtracting \code{geom_line()} will remove all calls matching 131 | #' \code{geom_line} regardless of their arguments. 132 | #' 133 | #' `labs()` has been identified as a special case, as it requires an argument in 134 | #' order to be recognised as a valid function. Thus, trying to remove it with an 135 | #' empty argument will fail. That said, the argument doesn't need to match, so 136 | #' it can be populated with a dummy string or anything that evaluates in scope. 137 | #' See examples. 138 | #' 139 | #' @param e1 An object of class \code{ggghost} 140 | #' @param e2 A component to remove from \code{e1} as either a string or a 141 | #' language object 142 | #' 143 | #' @return A \code{ggghost} structure with calls (text) matching \code{e2} 144 | #' removed, otherwise the same as \code{e1} 145 | #' 146 | #' @rdname minus-ggghost 147 | #' @export 148 | #' 149 | #' @examples 150 | #' ## create a ggghost object 151 | #' tmpdata <- data.frame(x = 1:100, y = rnorm(100)) 152 | #' 153 | #' z %g<% ggplot(tmpdata, aes(x,y)) 154 | #' z <- z + geom_point(col = "steelblue") 155 | #' z <- z + theme_bw() 156 | #' z <- z + labs(title = "My cool ggplot") 157 | #' z <- z + labs(x = "x axis", y = "y axis") 158 | #' z <- z + geom_smooth() 159 | #' 160 | #' z - "labs" # removes all labs 161 | #' z - "title" # removes just the title 162 | #' z - "axis" # removes the axis labels 163 | #' z - geom_point() # removes points 164 | #' z - theme_bw() # removes theme_bw() 165 | "-.gg" <- function(e1, e2) { 166 | if (ggplot2::is.theme(e1)) 167 | stop("not implemented for ggplot2 themes") else if (ggplot2::is.ggplot(e1)) 168 | stop("not implemented for ggplot2 plots") else if (is.ggghost(e1)) { 169 | call_to_remove <- match.call()[[3]] 170 | if ( 171 | !any(grepl( 172 | sub("\\(.*$", "", call_to_remove)[1], 173 | as.character(summary(e1, combine = TRUE)) 174 | )) 175 | ) { 176 | warning( 177 | "ggghostbuster: can't find that call in the call list", 178 | call. = FALSE 179 | ) 180 | return(e1) 181 | } else if (sub("\\(.*$", "", call_to_remove)[1] == "ggplot") { 182 | warning( 183 | "ggghostbuster: can't remove the ggplot call itself", 184 | call. = FALSE 185 | ) 186 | return(e1) 187 | } 188 | new_obj <- structure( 189 | unclass(e1)[-grep(sub("\\(.*$", "", call_to_remove)[1], unclass(e1))], 190 | class = c("ggghost", "gg") 191 | ) 192 | attr(new_obj, "data") <- attr(e1, "data") 193 | if (!is.null(attr(e1, "suppdata"))) { 194 | attr(new_obj, "suppdata") <- attr(e1, "suppdata") 195 | } 196 | return(new_obj) 197 | } 198 | } 199 | 200 | 201 | #' Collect ggghost calls and produce the ggplot output 202 | #' 203 | #' @param x A ggghost object to be made into a ggplot grob 204 | #' @param ... Not used, provided for \code{print.default} generic consistency. 205 | #' 206 | #' @return The ggplot plot data (invisibly). Used for the side-effect of producing a ggplot plot. 207 | #' @export 208 | print.ggghost <- function(x, ...) { 209 | recover_data(x, supp = TRUE) 210 | plotdata <- eval(parse(text = paste(x, collapse = " + "))) 211 | print(plotdata) 212 | return(invisible(plotdata)) 213 | } 214 | 215 | 216 | #' List the calls contained in a ggghost object 217 | #' 218 | #' Summarises a ggghost object by presenting the contained calls in the order 219 | #' they were added. Optionally concatenates these into a single ggplot call. 220 | #' 221 | #' @details The data is also included in ggghost objects. If this is also 222 | #' desired in the output, use \code{str}. See example. 223 | #' 224 | #' @param object A ggghost object to present 225 | #' @param ... Mainly provided for \code{summary.default} generic consistency. 226 | #' When \code{combine} is passed as an argument (arbitrary value) the list of 227 | #' calls is concatenated into a single string as one might write the ggplot 228 | #' call. 229 | #' 230 | #' @return Either a list of ggplot calls or a string of such concatenated with " + " 231 | #' @export 232 | #' 233 | #' @examples 234 | #' ## present the ggghost object as a list 235 | #' tmpdata <- data.frame(x = 1:100, y = rnorm(100)) 236 | #' 237 | #' z %g<% ggplot(tmpdata, aes(x,y)) 238 | #' z <- z + geom_point(col = "steelblue") 239 | #' summary(z) 240 | #' 241 | #' ## present the ggghost object as a string 242 | #' summary(z, combine = TRUE) # Note, value of 'combine' is arbitrary 243 | #' 244 | #' ## to inspect the data structure also captured, use str() 245 | #' str(z) 246 | summary.ggghost <- function(object, ...) { 247 | dots <- eval(substitute(alist(...))) 248 | combine = "combine" %in% names(dots) 249 | if (combine) return(paste(object, collapse = " + ")) else 250 | return(utils::head(object, n = length(object))) 251 | } 252 | 253 | 254 | #' Extract a subset of a ggghost object 255 | #' 256 | #' Alternative to subtracting calls using `-.gg`, this method allows one to 257 | #' select the desired components of the available calls and have those 258 | #' evaluated. 259 | #' 260 | #' @param x A ggghost object to subset 261 | #' @param ... A logical expression indicating which elements to select. 262 | #' Typically a vector of list numbers, but potentially a vector of logicals or 263 | #' logical expressions. 264 | #' 265 | #' @return Another ggghost object containing only the calls selected. 266 | #' @export 267 | #' 268 | #' @examples 269 | #' ## create a ggghost object 270 | #' tmpdata <- data.frame(x = 1:100, y = rnorm(100)) 271 | #' 272 | #' z %g<% ggplot(tmpdata, aes(x,y)) 273 | #' z <- z + geom_point(col = "steelblue") 274 | #' z <- z + theme_bw() 275 | #' z <- z + labs(title = "My cool ggplot") 276 | #' z <- z + labs(x = "x axis", y = "y axis") 277 | #' z <- z + geom_smooth() 278 | #' 279 | #' ## remove the labels and theme 280 | #' subset(z, c(1,2,6)) 281 | #' ## or 282 | #' subset(z, c(TRUE,TRUE,FALSE,FALSE,FALSE,TRUE)) 283 | subset.ggghost <- function(x, ...) { 284 | new_obj <- structure(unclass(x)[...], class = c("ggghost", "gg")) 285 | attr(new_obj, "data") <- attr(x, "data") 286 | if (!is.null(attr(x, "suppdata"))) { 287 | attr(new_obj, "suppdata") <- attr(x, "suppdata") 288 | } 289 | return(new_obj) 290 | } 291 | 292 | 293 | #' Bring a ggplot to life (re-animate) 294 | #' 295 | #' Creates an animation showing the stepwise process of building up a ggplot. 296 | #' Successively adds calls from a ggghost object and then combines these into an 297 | #' animated GIF. 298 | #' 299 | #' @param object A ggghost object to animate 300 | #' @param gifname Output filename to save the .gif to (not including any path, 301 | #' will be saved to current directory) 302 | #' @param interval A positive number to set the time interval of the animation 303 | #' (unit in seconds); see \code{animation::ani.options} 304 | #' @param ani.width width of image frames (unit in px); see 305 | #' \code{animation::ani.options} 306 | #' @param ani.height height of image frames (unit in px); see 307 | #' \code{animation::ani.options} 308 | #' 309 | #' @return \code{TRUE} if it gets that far 310 | #' 311 | #' @importFrom animation ani.options saveGIF 312 | #' @export 313 | #' @rdname reanimate 314 | #' 315 | #' @examples 316 | #' \dontrun{ 317 | #' ## create an animation showing the process of building up a plot 318 | #' reanimate(z, "mycoolplot.gif") 319 | #' } 320 | reanimate <- function( 321 | object, 322 | gifname = "ggghost.gif", 323 | interval = 1, 324 | ani.width = 600, 325 | ani.height = 600 326 | ) { 327 | stopifnot(length(object) > 1) 328 | animation::ani.options( 329 | interval = interval, 330 | ani.width = ani.width, 331 | ani.height = ani.height 332 | ) 333 | animation::saveGIF( 334 | { 335 | recover_data(object, supp = TRUE) 336 | ggtmp <- object[[1]] 337 | print(eval(ggtmp)) 338 | for (i in 2:length(object)) { 339 | ggtmp <- eval(ggtmp) + eval(object[[i]]) 340 | print(ggtmp) 341 | } 342 | }, 343 | movie.name = gifname 344 | ) 345 | return(invisible(TRUE)) 346 | } 347 | 348 | 349 | #' @export 350 | #' @rdname reanimate 351 | lazarus <- reanimate 352 | 353 | 354 | #' Recover data Stored in a ggghost object 355 | #' 356 | #' The data used to generate a plot is an essential requirement for a 357 | #' reproducible graphic. This is somewhat available from a ggplot \code{grob} 358 | #' (in raw form) but it it not easily accessible, and isn't named the same way 359 | #' as the original call. 360 | #' 361 | #' This function retrieves the data from the ggghost object as it was when it 362 | #' was originally called. 363 | #' 364 | #' If supplementary data has also been attached using \code{\link{supp_data}} 365 | #' then this will also be recovered (if requested). 366 | #' 367 | #' When used interactively, a warning will be produced if the data to be 368 | #' extracted exists in the workspace but not identical to the captured version. 369 | #' 370 | #' @param x A ggghost object from which to extract the data. 371 | #' @param supp (logical) Should the supplementary data be extracted also? 372 | #' 373 | #' @return A \code{data.frame} of the original data, named as it was when used 374 | #' in \code{ggplot(data)} 375 | #' @export 376 | recover_data <- function(x, supp = TRUE) { 377 | ## create a local copy of the data 378 | y <- yname <- attr(x, "data")$data_name 379 | assign(y, attr(x, "data")$data, envir = environment()) 380 | 381 | ## if the data exists in the calling frame, but has changed since 382 | ## being saved to the ggghost object, produce a warning (but do it anyway) 383 | parent <- parent.frame() 384 | optout_data <- "" 385 | if (exists(y, where = parent)) { 386 | if (!identical(get(y, envir = environment()), get(y, envir = parent))) { 387 | warning( 388 | paste0( 389 | "Potentially overwriting object ", 390 | yname, 391 | " in working space, but object has changed" 392 | ), 393 | call. = FALSE, 394 | immediate. = TRUE 395 | ) 396 | ## this should really be ggghost::in_the_shell as per @hrbrmstr's suggestion 397 | if (interactive()) { 398 | optout_data <- readline("Press 'n' to opt out of overwriting ") 399 | } 400 | } 401 | } 402 | 403 | if (optout_data != "n") assign(yname, attr(x, "data")$data, envir = parent) 404 | 405 | if (supp) { 406 | optout_supp_data <- "" 407 | supp_list <- supp_data(x) 408 | if (length(supp_list) > 0) { 409 | if (exists(supp_list[[1]], where = parent)) { 410 | if (!identical(supp_list[[2]], get(supp_list[[1]], envir = parent))) { 411 | warning( 412 | paste0( 413 | "Potentially overwriting object ", 414 | supp_list[[1]], 415 | " in working space, but object has changed" 416 | ), 417 | call. = FALSE, 418 | immediate. = TRUE 419 | ) 420 | if (interactive()) { 421 | optout_supp_data <- readline("Press 'n' to opt out of overwriting ") 422 | } 423 | } 424 | } 425 | if (optout_supp_data != "n") 426 | assign(supp_list[[1]], supp_list[[2]], envir = parent) 427 | } 428 | } 429 | 430 | return(invisible(NULL)) 431 | } 432 | 433 | 434 | #' Inspect the supplementary data attached to a ggghost object 435 | #' 436 | #' @param x A ggghost object 437 | #' 438 | #' @return A list with two elements: the name of the supplementary data, and the 439 | #' supplementary data itself 440 | #' 441 | #' @export 442 | supp_data <- function(x) { 443 | value <- attr(x, "suppdata") 444 | # if (length(value) == 0 & interactive()) warning("ggghostbuster: no supplementary data found", call. = FALSE) 445 | 446 | return(value) 447 | } 448 | 449 | #' Attach supplementary data to a ggghost object 450 | #' 451 | #' @param x A ggghost object to which the supplementary data should be 452 | #' attached 453 | #' @param value Supplementary data to attach to the ggghost object, probably 454 | #' used as an additional data input to a \code{scale_*} or \code{geom_*} call 455 | #' 456 | #' @return The original object with \code{suppdata} attribute 457 | #' 458 | #' @export 459 | "supp_data<-" <- function(x, value) { 460 | if (is.ggghost(x)) { 461 | if (length(attr(x, "suppdata")) > 0) { 462 | warning( 463 | "ggghostbuster: can't assign more than one supplementary data set to a ggghost object.", 464 | call. = FALSE 465 | ) 466 | return(x) 467 | } 468 | 469 | attr(x, "suppdata") <- list( 470 | supp_data_name = as.character(substitute(value)), 471 | supp_data = value 472 | ) 473 | } else { 474 | stop("attempt to attach supplementary data to a non-ggghost object") 475 | } 476 | 477 | return(x) 478 | } 479 | --------------------------------------------------------------------------------