
StatDensity
#> <ggproto object: Class StatDensity, Stat, gg>
#> aesthetics: function
#> compute_group: function
#> compute_layer: function
#> compute_panel: function
#> finish_layer: function
#> parameters: function
#> setup_data: function
#> setup_params: function
#> default_aes: ggplot2::mapping, uneval, gg, S7_object
#> dropped_aes: weight
#> extra_params: na.rm orientation
#> non_missing_aes:
#> optional_aes:
#> required_aes: x|y
#> retransform: TRUE
#> super: <ggproto object: Class Stat, gg>Primer on modify-in-place
# Creating modifiable copy of Stat
StatGood <- ggproto(NULL, Stat)
# Changing a field
StatGood <- ggproto(NULL, Stat, required_aes = "x")
Stat$required_aes
## character(0)
# This does NOT copy Stat
StatBad <- Stat
# Modify-in-place shenanigans
StatBad$required_aes <- "x"
Stat$required_aes
## [1] "x"
# Never circularly define a ggproto object
# Stat <- ggproto(NULL, Stat)Methods have access to the class object itself via a self variable if it is included as an argument in the method. It can be used to read fields and use other methods.
Stat$aesthetics
## <ggproto method>
## <Wrapper function>
## function (...)
## aesthetics(..., self = self)
##
## <Inner function (f)>
## function (self)
## {
## if (is.null(self$required_aes)) {
## required_aes <- NULL
## }
## else {
## required_aes <- unlist(strsplit(self$required_aes, "|",
## fixed = TRUE))
## }
## c(union(required_aes, names(self$default_aes)), self$optional_aes,
## "group")
## }
StatDensity$aesthetics()
## [1] "x" "y" "fill" "weight" "group"Extendible classes are stateless: fields don’t mutate during plot building. State is primarily encoded in the data, and secondarily in params managed by ggplot2’s internals. Fields should be ‘read only’.
Input is evaluated aesthetics in a data frame. Output is an amended data frame with computed variables.
This adds fitted values and residuals from a linear model to data as computed variables. Similar to a bare bone broom::augment(). It assumes the presence of an x and y variable.
You can test the compute function outside ggplot to convince yourself it is doing the right thing. Using a separate function is also easier to debug.
We create a Stat subclass using our function as the compute_group method.
Error in `geom_segment()`:
! Problem while setting up geom.
ℹ Error occurred in the 3rd layer.
Caused by error in `compute_geom_1()`:
! `geom_segment()` requires the following missing aesthetics: xend or
yend.
To resolve friction, we can try fixing it on the user-side.
But in this case we can provide the missing aesthetic as a default from the computed variables.
We may need to formalise any required aesthetics, or in some cases: list optional aesthetics.
StatResidual <- ggproto(
"StatResidual",
Stat,
compute_group = residual_lines,
default_aes = aes(
yend = after_stat(fitted)
),
# As mentioned before, the compute
# function assumes the presence
# of `x` and `y` variables
required_aes = c("x", "y"),
# This example doesn't have
# optional aesthetics
optional_aes = character()
)
p + geom_segment(stat = StatResidual)
We can re-assure ourselves that our Stat behaves correctly when the data has groups.
A few considerations:
compute_group() when group-level stats are required.compute_panel() when computing within single panels.
compute_group()compute_layer() unless you have no other options.
compute_panel().The methods can be debugged with ggplot2:::ggproto_debug(StatResidual$compute_group).
A good start is to use other constructors as a template.
stat_boxplot
#> function(
#> mapping = NULL,
#> data = NULL,
#> geom = "boxplot",
#> position = "dodge2",
#> ...,
#> orientation = NA,
#> coef = 1.5,
#> na.rm = FALSE,
#> show.legend = NA,
#> inherit.aes = TRUE
#> ) {
#> layer(
#> mapping = mapping,
#> data = data,
#> geom = geom,
#> stat = "boxplot",
#> position = position,
#> show.legend = show.legend,
#> inherit.aes = inherit.aes,
#> params = list2(
#> na.rm = na.rm,
#> orientation = orientation,
#> coef = coef,
#> ...
#> )
#> )
#> }mapping and data. Every layer needs geom, stat and position. A stat_* constructor omits the stat argument because that will be provided for you. A geom_*() constructor omits the geom argument.
... argument, which requires users to write the argument names out in full.
na.rm, show.legend and inherit.aes arguments come last and should have these default values in most cases. If you’re making an annotate_*() layer, you may put inherit.aes = FALSE for example.
?layer documentation to see what are the standard arguments.
rlang::list2() because it supports argument splicing.
na.rm argument, all parameters to the Stat and ... gets passed to the layer(params) argument.
stat_*() constructor, the layer(stat) argument is fixed. In a geom_*() constructor, the layer(geom) argument is fixed.
When we make our own constructor, we follow the same rules.
stat_residual <- function(
mapping = NULL,
data = NULL,
geom = "segment",
position = "identity",
...,
formula = y ~ x,
na.rm = FALSE,
show.legend = NA,
inherit.aes = TRUE
) {
layer(
mapping = mapping,
data = data,
geom = geom,
stat = StatResidual,
position = position,
show.legend = show.legend,
inherit.aes = inherit.aes,
params = rlang::list2(
na.rm = na.rm,
formula = formula,
...
)
)
}
p + stat_residual()mapping and data. Every layer needs geom, stat and position. A stat_* constructor omits the stat argument because that will be provided for you. A geom_*() constructor omits the geom argument.
... argument, which requires users to write the argument names out in full.
na.rm, show.legend and inherit.aes arguments come last and should have these default values in most cases. If you’re making an annotate_*() layer, you may put inherit.aes = FALSE for example.
?layer documentation to see what are the standard arguments.
rlang::list2() because it supports argument splicing.
na.rm argument, all parameters to the Stat and ... gets passed to the layer(params) argument.
stat_*() constructor, the layer(stat) argument is fixed. In a geom_*() constructor, the layer(geom) argument is fixed.

Instead of following all the rules, you can also use cookie-cutter make_constructor().
stat_residual <- make_constructor(StatResidual, geom = "segment")
print(stat_residual)
## function (mapping = NULL, data = NULL, geom = "segment", position = "identity",
## ..., formula = y ~ x, na.rm = FALSE, show.legend = NA, inherit.aes = TRUE)
## {
## layer(mapping = mapping, data = data, geom = geom, stat = "residual",
## position = position, show.legend = show.legend, inherit.aes = inherit.aes,
## params = list2(na.rm = na.rm, formula = formula, ...))
## }
## <environment: 0x560775a0df18>
p + stat_residual()
You can use the setup_params() for:
stat_smooth() tries to find valid method.stat_bin() watches for deprecated arguments.stat_contour() tracks range of z aesthetic to re-use in group-level computation.orientation
You can use the setup_data() method for:
stat_boxplot() removes NA values.stat_contour() cannot have duplicate data.StatBoxplot$setup_data
## <ggproto method>
## <Wrapper function>
## function (...)
## setup_data(..., self = self)
##
## <Inner function (f)>
## function (self, data, params)
## {
## data <- flip_data(data, params$flipped_aes)
## data$x <- data$x %||% 0
## data <- remove_missing(data, na.rm = params$na.rm, vars = "x",
## name = "stat_boxplot")
## flip_data(data, params$flipped_aes)
## }Stat* ggproto class
compute_layer()/compute_panel()/compute_group() methoddefault_aessetup_data()/setup_params()make_constructor()05:00
draw_group()draw_panel()draw_layer()make_constructor() too.draw_key_*() functions.ggforce::facet_matrix()ggraph::facet_nodes()ggh4x::facet_manual()ggh4x::facet_nested()ggforce::facet_zoom()Next session: Spice up your plot