Skip to contents

Insert traces for delayed evaluation

Usage

ggtrace(
  method,
  trace_steps,
  trace_exprs,
  once = TRUE,
  use_names = TRUE,
  ...,
  print_output = TRUE,
  verbose = FALSE
)

Arguments

method

A function or a ggproto method. The ggproto method may be specified using any of the following forms:

  • ggproto$method

  • namespace::ggproto$method

  • namespace:::ggproto$method

trace_steps

A sorted numeric vector of positions in the method's body to trace. Negative indices reference steps from the last, where -1 references the last step in the body. Special value "all" traces all steps of the method body.

trace_exprs

A list of expressions to evaluate at each position specified in trace_steps. If a single expression is provided, it is recycled to match the length of trace_steps.

To simply run a step and return its output, you can use the ~step keyword. If the step is an assign expression, the value of the assigned variable is returned. If trace_exprs is not provided, ggtrace() is called with ~step by default.

once

Whether to untrace() the method on exit. If FALSE, creates a persistent trace which is active until gguntrace() is called on the method. Defaults to TRUE.

use_names

Whether the trace dump should use the names from trace_exprs. If trace_exprs is not specified, whether to use the method steps as names. Defaults to TRUE.

...

Unused, for extensibility.

print_output

Whether to print() the output of each expression to the console. Defaults to TRUE.

verbose

Whether logs should be printed when trace is triggered. Encompasses print_output, meaning that verbose = FALSE also triggers the effect of print_output = FALSE by consequence. Defaults to FALSE.

Details

ggtrace() is a wrapper around base::trace() which is called on the ggproto method. It calls base::untrace() on itself on exit by default, to make its effect ephemeral like base::debugonce(). A major feature is the ability to pass multiple positions and expressions to trace_steps and trace_exprs to inspect, capture, and modify the run time environment of ggproto methods. It is recommended to consult the output of ggbody() when deciding which expressions to evaluate at which steps.

The output of the expressions passed to trace_exprs is printed while tracing takes place. The list of outputs from ggtrace() ("trace dumps") can be returned for further inspection with last_ggtrace() or global_ggtrace().

Workflows

Broadly, there are four flavors of working with the {ggtrace} package, listed in the order of increasing complexity:

  • Inspect: The canonical use of ggtrace() to make queries, where expressions are passed in and their evaluated output are returned, potentially for further inspection.

  • Capture: The strategy of returning the method's runtime environment for more complex explorations outside of the debugging context. A method's environment contextualizes the self object in addition to making all inherited params and local variables available.

    A reference to the method's runtime environment can be returned with environment(), as in trace_exprs = quote(environment()). Note that environments are mutable, meaning that environment() returned from the first and last steps will reference the same environment. To get a snapshot of the environment at a particular step, you can return a deep copy with rlang::env_clone(environment()).

  • Inject: The strategy of modifying the behavior of a method as it runs by passing in expressions that make assignments.

    For example, trace_steps = c(1, 10) with trace_exprs = rlang::exprs(a <- 5, a) will first assign a new variable a at step 1, and return its value 5 at step 10. This can also be used to modify important variables like quote(data$x <- data$x * 10). If you would like to inject an object from the global environment, you can make use of the !! (bang-bang) operator from {rlang}, like so: rlang::expr(data <- !!modified_data).

    Note that the execution environment is created anew each time the method is ran, so modifying the environment from its previous execution will not affect future calls to the method.

    If you would like to capture the modified plot output and assign it to a variable, you can do so with ggplotGrob(). You can then render the modified plot with print().

  • Edit: It is also possible to make any arbitrary modifications to the method's source code, which stays in effect until the method is untraced. While this is also handled with base::trace(), this workflow is fundamentally interactive. Therefore, it has been refactored as its own function ggedit(). See ?ggedit for more details.

Gotchas

  • If you wrap a ggplot in invisible() to silence ggtrace(), the plot will not build, which also means that the tracing is not triggered. This is because the print/plot method of ggplot is what triggers the evaluation of the plot code. It is recommended to allow ggtrace() to print information, but if you'd really like to silence it, you can do so by wrapping the plot in a function that forces its evaluation first, like ggplotGrob, as in invisible(ggplotGrob(<plot>)).

  • If for any reason ggtrace(once = TRUE) fails to untrace itself on exit, you may accidentally trigger the trace again. To check if a method is being traced, call is_traced(). You can also always call gguntrace() since unlike base::untrace(), it will not error if a trace doesn't exist on the method. Instead, gguntrace() will do nothing in that case and simply inform you that there is no trace to remove.

  • Because base::trace() wraps the method body in a special environment, it is not possible to inspect the method/function which called it, even with something like rlang::caller_env(). You will traverse through a few wrapping environments created by base::trace() which eventually ends up looping around.

Messages

Various information is sent to the console whenever a trace is triggered. You can control what gets displayed with print_output and verbose, which are both TRUE by default. print_output simply calls print() on the evaluated expressions, and turning this off may be desirable if expressions in trace_exprs evaluates to a long dataframe or vector. verbose controls all information printed to the console including those by print(), and setting verbose = FALSE will mean that only message()s will be displayed. Lastly, you can suppress message() from ggtrace() with options(ggtrace.suppressMessages = TRUE), though suppressing messages is generally not recommended.

Examples

# One example of an Inspect workflow ----

library(ggplot2)

jitter_plot <- ggplot(diamonds[1:1000,], aes(cut, depth)) +
  geom_point(position = position_jitter(width = 0.2, seed = 2021))

jitter_plot


ggbody(PositionJitter$compute_layer)
#> [[1]]
#> `{`
#> 
#> [[2]]
#> trans_x <- if (params$width > 0) function(x) jitter(x, amount = params$width)
#> 
#> [[3]]
#> trans_y <- if (params$height > 0) function(x) jitter(x, amount = params$height)
#> 
#> [[4]]
#> x_aes <- intersect(ggplot_global$x_aes, names(data))
#> 
#> [[5]]
#> x <- if (length(x_aes) == 0) 0 else data[[x_aes[1]]]
#> 
#> [[6]]
#> y_aes <- intersect(ggplot_global$y_aes, names(data))
#> 
#> [[7]]
#> y <- if (length(y_aes) == 0) 0 else data[[y_aes[1]]]
#> 
#> [[8]]
#> dummy_data <- data_frame0(x = x, y = y, .size = nrow(data))
#> 
#> [[9]]
#> fixed_jitter <- with_seed_null(params$seed, transform_position(dummy_data, 
#>     trans_x, trans_y))
#> 
#> [[10]]
#> x_jit <- fixed_jitter$x - x
#> 
#> [[11]]
#> y_jit <- fixed_jitter$y - y
#> 
#> [[12]]
#> x_jit[is.infinite(x)] <- 0
#> 
#> [[13]]
#> y_jit[is.infinite(y)] <- 0
#> 
#> [[14]]
#> transform_position(data, function(x) x + x_jit, function(x) x + 
#>     y_jit)
#> 

## Step 1 ====
## Inspect what `data` look like at the start of the function
ggtrace(PositionJitter$compute_layer, trace_steps = 1, trace_exprs = quote(head(data)))
#> `PositionJitter$compute_layer` now being traced.

jitter_plot
#> Triggering trace on `PositionJitter$compute_layer`
#> Untracing `PositionJitter$compute_layer` on exit.


## Step 2 ====
## What does `data` look like at the end of the method? Unfortunately, `trace()` only lets us enter
## at the beginning of a step, so we can't inspect what happens after the last step is evaluated. To
## address this, `ggtrace()` offers a `~step` keyword which gets substituted for the current line.
## We also set `print_output = FALSE` to disable printing of the output
ggtrace(
  PositionJitter$compute_layer,
  trace_steps = 14,
  trace_exprs = quote(~step), # This is the default if `trace_exprs` is not provided
  print_output = FALSE
)
#> `PositionJitter$compute_layer` now being traced.

# We wrap the plot in `ggplotGrob()` and `invisible()` to force
# its evaluation while suppressing its rendering
invisible(ggplotGrob(jitter_plot))
#> Triggering trace on `PositionJitter$compute_layer`
#> Untracing `PositionJitter$compute_layer` on exit.

# The output of the evaluated expressions can be inspected with `last_ggtrace()`
head(last_ggtrace()[[1]])
#>          x        y PANEL group
#> 1 4.980507 61.50684     1     5
#> 2 4.113512 59.77872     1     4
#> 3 2.083873 56.86655     1     2
#> 4 3.952698 62.42703     1     4
#> 5 2.054530 63.29763     1     2
#> 6 3.080538 62.77536     1     3

## Step 3 ====
## If we want both to be returned at the same time for an easier comparison, we can pass in a
## (named) list of expressions.
ggtrace(
  PositionJitter$compute_layer,
  trace_steps = c(1, 14),
  trace_exprs = rlang::exprs(
    before_jitter = data,
    after_jitter = ~step
  ),
  verbose = FALSE
)
#> `PositionJitter$compute_layer` now being traced.

invisible(ggplotGrob(jitter_plot))
#> Triggering trace on `PositionJitter$compute_layer`
#> Untracing `PositionJitter$compute_layer` on exit.

## Step 4 ====
## The output of the evaluated expressions can be inspected with `last_ggtrace()`
jitter_tracedump <- last_ggtrace()

lapply(jitter_tracedump, head, 3)
#> $before_jitter
#>   x    y PANEL group
#> 1 5 61.5     1     5
#> 2 4 59.8     1     4
#> 3 2 56.9     1     2
#> 
#> $after_jitter
#>          x        y PANEL group
#> 1 4.980507 61.50684     1     5
#> 2 4.113512 59.77872     1     4
#> 3 2.083873 56.86655     1     2
#> 

jitter_distances <- jitter_tracedump[["before_jitter"]]$x -
  jitter_tracedump[["after_jitter"]]$x

range(jitter_distances)
#> [1] -0.1994971  0.1995346
jitter_plot$layers[[1]]$position$width
#> [1] 0.2