ggtrace()
is essentially just a wrapper around
base::trace()
designed to make it easy and safe to
programmatically trace/untrace functions and methods.
So the short answer is that ggtrace()
is at least as
safe as trace()
. But how safe is
trace()
?
The beauty of trace()
is that the modified function
being traced masks over the original function without
overwriting it. This allows for non-destructive
modifications to the execution behavior.
In this simple example, we add a trace to replace()
which multiples the value of x
by 10 as the function enters
the last step.
body(replace)
#> {
#> x[list] <- values
#> x
#> }
replace(1:5, 3, 30)
#> [1] 1 2 30 4 5
as.list(body(replace))
#> [[1]]
#> `{`
#>
#> [[2]]
#> x[list] <- values
#>
#> [[3]]
#> x
trace(replace, tracer = quote(x <- x * 10), at = 3)
#> Tracing function "replace" in package "base"
#> [1] "replace"
The traced function looks strange and runs with a different behavior
class(replace)
#> [1] "functionWithTrace"
#> attr(,"package")
#> [1] "methods"
body(replace)
#> {
#> x[list] <- values
#> {
#> .doTrace(x <- x * 10, "step 3")
#> x
#> }
#> }
replace(1:5, 3, 30)
#> Tracing replace(1:5, 3, 30) step 3
#> [1] 10 20 300 40 50
But again, this is non-destructive. The original function body is
safely stored away in the "original"
attribute of the
traced function
attr(replace, "original")
#> function (x, list, values)
#> {
#> x[list] <- values
#> x
#> }
#> <bytecode: 0x55a02f058240>
#> <environment: namespace:base>
The original function can be recovered by removing the trace with a
call to untrace()
untrace(replace)
#> Untracing function "replace" in package "base"
body(replace)
#> {
#> x[list] <- values
#> x
#> }
replace(1:5, 3, 30)
#> [1] 1 2 30 4 5
Beyond this, {ggtrace}
also offers some extra built-in
safety measures:
once = TRUE
)is_traced()
)However, some expression you pass to ggtrace()
for
delayed evaluation are not without consequences. You need to be careful
about running functions that have side effects and making
assignments to environments (ex:
self$method <- ...
will modify in place). But this isn’t
a problem of {ggtrace}
- they follow from the general rules
of reference semantics in R.
ggtrace()
?{base} functions
sample(letters, 5)
#> [1] "g" "f" "n" "z" "l"
ggtrace(sample, 1, quote(x <- LETTERS), verbose = FALSE)
#> `sample` now being traced.
sample(letters, 5)
#> Triggering trace on `sample`
#> Untracing `sample` on exit.
#> [1] "T" "F" "Y" "X" "N"
sample(letters, 5)
#> [1] "e" "o" "g" "i" "l"
Imported functions
ggtrace(ggplot2::mean_se, 1, quote(cat("Running...\n")), verbose = FALSE)
#> `ggplot2::mean_se` now being traced.
ggplot2::mean_se(mtcars$mpg)
#> Triggering trace on `ggplot2::mean_se`
#> Running...
#> Untracing `ggplot2::mean_se` on exit.
#> y ymin ymax
#> 1 20.09062 19.0252 21.15605
Custom functions
please_return_number <- function() {
result <- runif(1)
result
}
please_return_number()
#> [1] 0.5456977
ggtrace(please_return_number, -1, quote(result <- "no"), verbose = FALSE)
#> `please_return_number` now being traced.
please_return_number()
#> Triggering trace on `please_return_number`
#> Untracing `please_return_number` on exit.
#> [1] "no"
Default tracing behavior with untracing on exit
ggtrace(StatBoxplot$compute_group, -1, verbose = FALSE)
#> `StatBoxplot$compute_group` now being traced.
# Plot not printed to save space
boxplot_plot
#> Triggering trace on `StatBoxplot$compute_group`
#> Untracing `StatBoxplot$compute_group` on exit.
last_ggtrace()
#> $`[Step 19]> flip_data(df, flipped_aes)`
#> ymin lower middle upper ymax outliers notchupper notchlower x width
#> 1 12 17 18 22 28 18.77841 17.22159 1 0.75
#> relvarwidth flipped_aes
#> 1 10.14889 FALSE
Persistent trace with once = FALSE
and explicit
untracing with gguntrace()
global_ggtrace_state()
#> [1] FALSE
global_ggtrace_state(TRUE)
#> Global tracedump activated.
clear_global_ggtrace()
#> Global tracedump cleared.
ggtrace(StatBoxplot$compute_group, -1, once = FALSE, verbose = FALSE)
#> `StatBoxplot$compute_group` now being traced.
#> Creating a persistent trace. Remember to `gguntrace(StatBoxplot$compute_group)`!
# Plot not printed to save space
boxplot_plot
#> Triggering persistent trace on `StatBoxplot$compute_group`
#> Triggering persistent trace on `StatBoxplot$compute_group`
#> Triggering persistent trace on `StatBoxplot$compute_group`
gguntrace(StatBoxplot$compute_group)
#> `StatBoxplot$compute_group` no longer being traced.
global_ggtrace()
#> $`StatBoxplot$compute_group-0x55a031745aa0`
#> $`StatBoxplot$compute_group-0x55a031745aa0`$`[Step 19]> flip_data(df, flipped_aes)`
#> ymin lower middle upper ymax outliers notchupper notchlower x width
#> 1 12 17 18 22 28 18.77841 17.22159 1 0.75
#> relvarwidth flipped_aes
#> 1 10.14889 FALSE
#>
#>
#> $`StatBoxplot$compute_group-0x55a03260f770`
#> $`StatBoxplot$compute_group-0x55a03260f770`$`[Step 19]> flip_data(df, flipped_aes)`
#> ymin lower middle upper ymax outliers
#> 1 22 26 28 29 33 17, 21, 34, 36, 36, 35, 37, 35, 44, 44, 41
#> notchupper notchlower x width relvarwidth flipped_aes
#> 1 28.46039 27.53961 2 0.75 10.29563 FALSE
#>
#>
#> $`StatBoxplot$compute_group-0x55a032f5c4a0`
#> $`StatBoxplot$compute_group-0x55a032f5c4a0`$`[Step 19]> flip_data(df, flipped_aes)`
#> ymin lower middle upper ymax outliers notchupper notchlower x width
#> 1 15 17 21 24 26 23.212 18.788 3 0.75
#> relvarwidth flipped_aes
#> 1 5 FALSE
global_ggtrace_state(FALSE)
#> Global tracedump deactivated.
The exported generic function ggplot_build()
from
{ggplot2}
is itself not very meaningful, but the unexported
method for the <ggplot>
class
ggplot_build.ggplot()
contains the actual data
transformation pipeline.
body(ggplot_build)
#> {
#> attach_plot_env(plot$plot_env)
#> UseMethod("ggplot_build")
#> }
attr(utils::methods("ggplot_build"), "info")
#> visible from generic
#> ggplot_build.ggplot FALSE registered S3method for ggplot_build ggplot_build
#> isS4
#> ggplot_build.ggplot FALSE
You can trace the ggplot_build()
method defined for
<ggplot>
in the same way as functions
ggtrace(ggplot2:::ggplot_build.ggplot, -1, verbose = FALSE)
#> `ggplot2:::ggplot_build.ggplot` now being traced.
boxplot_plot
#> Triggering trace on `ggplot2:::ggplot_build.ggplot`
#> Untracing `ggplot2:::ggplot_build.ggplot` on exit.
last_ggtrace()[[1]]$data[[1]]
#> ymin lower middle upper ymax outliers
#> 1 12 17 18 22 28
#> 2 22 26 28 29 33 17, 21, 34, 36, 36, 35, 37, 35, 44, 44, 41
#> 3 15 17 21 24 26
#> notchupper notchlower x flipped_aes PANEL group ymin_final ymax_final xmin
#> 1 18.77841 17.22159 1 FALSE 1 1 12 28 0.625
#> 2 28.46039 27.53961 2 FALSE 1 2 17 44 1.625
#> 3 23.21200 18.78800 3 FALSE 1 3 15 26 2.625
#> xmax xid newx new_width weight colour fill alpha shape linetype linewidth
#> 1 1.375 1 1 0.75 1 grey20 white NA 19 solid 0.5
#> 2 2.375 2 2 0.75 1 grey20 white NA 19 solid 0.5
#> 3 3.375 3 3 0.75 1 grey20 white NA 19 solid 0.5
identical(last_ggtrace()[[1]]$data[[1]], layer_data(boxplot_plot, 1))
#> [1] TRUE
Adopted from Advanced R Ch. 14.2
library(R6)
Accumulator <- R6Class("Accumulator", list(
sum = 0,
add = function(x = 1) {
self$sum <- self$sum + x
invisible(self)
})
)
x <- Accumulator$new()
x$add(1)
x$sum
#> [1] 1
ggtrace(
method = x$add,
trace_steps = c(1, -1),
trace_exprs = list(
before = quote(self$sum),
after = quote(self$sum)
),
once = FALSE,
verbose = FALSE
)
#> `x$add` now being traced.
#> Creating a persistent trace. Remember to `gguntrace(x$add)`!
x$add(10)
#> Triggering persistent trace on `x$add`
last_ggtrace()
#> $before
#> [1] 1
#>
#> $after
#> [1] 11
x$add(100)
#> Triggering persistent trace on `x$add`
last_ggtrace()
#> $before
#> [1] 11
#>
#> $after
#> [1] 111
gguntrace(x$add)
#> `x$add` no longer being traced.
x$add(1000)
x$sum
#> [1] 1111
ggtrace()
?ggbody()
ggtrace()
)$
must
itself be an environment where the function can be searched for)When you trace the internals of ggplot, that doesn’t directly modify the instructions for plotting. Instead, it changes how certain components behave when they are executed.
This means that you will not get a different ggplot with the
following code if original_plot
is being traced with
modifications, since original_plot
is not being executed
here.
original_plot <- ggplot(mtcars, aes(hp, mpg)) + geom_point()
ggtrace(ggplot2:::ggplot_build.ggplot, -1, quote(data[[1]]$colour <- "red"), verbose = FALSE)
#> `ggplot2:::ggplot_build.ggplot` now being traced.
modified_plot <- original_plot
It looks like it worked when you first print it…
modified_plot
#> Triggering trace on `ggplot2:::ggplot_build.ggplot`
#> Untracing `ggplot2:::ggplot_build.ggplot` on exit.
But the variable modified_pot
doesn’t hold modified
code for generating the plot. Instead, it just happened to
trigger the trace on ggplot_build.ggplot(). So the next time it runs,
it’s ran with the normal behavior of original_plot
.
To capture the actual figure generated by a ggplot, you can use
ggplotGrob()
, which returns the Graphical
object representation of the plot:
ggtrace(ggplot2:::ggplot_build.ggplot, -1, quote(data[[1]]$colour <- "red"), verbose = FALSE)
#> `ggplot2:::ggplot_build.ggplot` now being traced.
modified_plot <- ggplotGrob(original_plot)
#> Triggering trace on `ggplot2:::ggplot_build.ggplot`
#> Untracing `ggplot2:::ggplot_build.ggplot` on exit.
modified_plot
#> TableGrob (16 x 13) "layout": 22 grobs
#> z cells name
#> 1 0 ( 1-16, 1-13) background
#> 2 5 ( 8- 8, 6- 6) spacer
#> 3 7 ( 9- 9, 6- 6) axis-l
#> 4 3 (10-10, 6- 6) spacer
#> 5 6 ( 8- 8, 7- 7) axis-t
#> 6 1 ( 9- 9, 7- 7) panel
#> 7 9 (10-10, 7- 7) axis-b
#> 8 4 ( 8- 8, 8- 8) spacer
#> 9 8 ( 9- 9, 8- 8) axis-r
#> 10 2 (10-10, 8- 8) spacer
#> 11 10 ( 7- 7, 7- 7) xlab-t
#> 12 11 (11-11, 7- 7) xlab-b
#> 13 12 ( 9- 9, 5- 5) ylab-l
#> 14 13 ( 9- 9, 9- 9) ylab-r
#> 15 14 ( 9- 9,11-11) guide-box-right
#> 16 15 ( 9- 9, 3- 3) guide-box-left
#> 17 16 (13-13, 7- 7) guide-box-bottom
#> 18 17 ( 5- 5, 7- 7) guide-box-top
#> 19 18 ( 9- 9, 7- 7) guide-box-inside
#> 20 19 ( 4- 4, 7- 7) subtitle
#> 21 20 ( 3- 3, 7- 7) title
#> 22 21 (14-14, 7- 7) caption
#> grob
#> 1 rect[plot.background..rect.350]
#> 2 zeroGrob[NULL]
#> 3 absoluteGrob[GRID.absoluteGrob.339]
#> 4 zeroGrob[NULL]
#> 5 zeroGrob[NULL]
#> 6 gTree[panel-1.gTree.331]
#> 7 absoluteGrob[GRID.absoluteGrob.335]
#> 8 zeroGrob[NULL]
#> 9 zeroGrob[NULL]
#> 10 zeroGrob[NULL]
#> 11 zeroGrob[NULL]
#> 12 titleGrob[axis.title.x.bottom..titleGrob.342]
#> 13 titleGrob[axis.title.y.left..titleGrob.345]
#> 14 zeroGrob[NULL]
#> 15 zeroGrob[NULL]
#> 16 zeroGrob[NULL]
#> 17 zeroGrob[NULL]
#> 18 zeroGrob[NULL]
#> 19 zeroGrob[NULL]
#> 20 zeroGrob[plot.subtitle..zeroGrob.347]
#> 21 zeroGrob[plot.title..zeroGrob.346]
#> 22 zeroGrob[plot.caption..zeroGrob.348]
What you get is an object of class <gtable>
, which
you can draw to your device like any other grob:
class(modified_plot)
#> [1] "gtable" "gTree" "grob" "gDesc"
library(grid)
grid.newpage()
grid.draw(modified_plot)
You can also use ggsave() to render a <gtable>
to
an image:
Still, modified_plot
is only the graphical
representation of the plot and not itself a ggplot object so you can’t
keep adding layers to it. So grobs are more limiting in that sense.
But it’s not totally limiting like a raster image of a figure. For
example, {patchwork}
has
patchwork::wrap_ggplot_grob()
which allows a
<gtable>
to be properly aligned to other ggplots.