Nothing
# gf_model() with no model draws the model the plot implies. Every test names the
# mutation it detects, per the file's own header rule. No cross-file state:
# `built()` is defined locally rather than shared, because testthat runs in
# parallel.
built <- function(p) ggplot2::ggplot_build(p)$data[[layer_index(p, "model")]]
sorted_cols <- function(df, cols) {
df <- df[cols]
df <- df[order(df$PANEL, df$x), ]
rownames(df) <- NULL
df
}
test_that("a categorical predictor's group marks agree with the named model, over four types", {
Fingers$sex_chr <- as.character(Fingers$Sex)
Fingers$sex_lgl <- Fingers$Sex == "female"
predictors <- list(Sex = Fingers$Sex, Job = Fingers$Job, sex_chr = Fingers$sex_chr,
sex_lgl = Fingers$sex_lgl)
for (name in names(predictors)) {
inferred <- gf_jitter(as.formula(paste("Thumb ~", name)), data = Fingers, width = .1) %>%
gf_model()
named <- gf_jitter(as.formula(paste("Thumb ~", name)), data = Fingers, width = .1) %>%
gf_model(lm(as.formula(paste("Thumb ~", name)), data = Fingers))
b_inferred <- built(inferred)
expect_equal(unname(b_inferred$y), as.numeric(tapply(Fingers$Thumb, predictors[[name]], mean)),
info = name)
# Compare the position columns that define the marks. Logical groups can
# arrive in a different row order, so normalize both sides first.
cols <- c("x", "xend", "y", "yend", "PANEL")
expect_equal(sorted_cols(b_inferred, cols), sorted_cols(built(named), cols), info = name)
}
})
test_that("the categorical mark uses the shared model stat and geom", {
p <- gf_jitter(Thumb ~ Sex, data = Fingers, width = .1) %>% gf_model()
layer <- p$layers[[layer_index(p, "model")]]
expect_s3_class(layer$geom, "GeomModel")
expect_s3_class(layer$stat, "StatModel")
})
test_that("a continuous predictor's line is the regression the plot implies", {
p <- gf_point(Thumb ~ Height, data = Fingers) %>% gf_model()
fitted <- lm(y ~ x, built(p))
named <- lm(Thumb ~ Height, data = Fingers)
# catches fitting the wrong variables, the wrong rows, or drawing a mean
# line instead of a fit
expect_equal(unname(coef(fitted)), unname(coef(named)), tolerance = 1e-8)
})
test_that("an inferred line names its own formula instead of ggplot2 guessing one", {
# MUTATION: leaving `formula` out of the line's params, which lets
# StatSmooth infer its own default and print "`geom_smooth()` using
# formula = 'y ~ x'" -- naming a function the caller never called
expect_silent(ggplot2::ggplot_build(gf_point(Thumb ~ Height, data = Fingers) %>% gf_model()))
})
test_that("gf_lm()'s fitting vocabulary reaches the inferred line through ...", {
inferred <- gf_point(Thumb ~ Height, data = Fingers) %>%
gf_model(formula = y ~ poly(x, 2))
named <- suppressMessages(gf_lm(Thumb ~ Height, data = Fingers, formula = y ~ poly(x, 2)))
# catches adding a `formula` formal that shadows it, or a layer function that
# drops params the way model_layer_fun() deliberately does
expect_equal(built(inferred)$y, ggplot2::ggplot_build(named)$data[[1]]$y)
inferred_se <- gf_point(Thumb ~ Height, data = Fingers) %>% gf_model(se = TRUE)
named_ci <- suppressMessages(gf_lm(Thumb ~ Height, data = Fingers, interval = "confidence"))
expect_equal(built(inferred_se)$ymin, ggplot2::ggplot_build(named_ci)$data[[1]]$ymin)
expect_equal(built(inferred_se)$ymax, ggplot2::ggplot_build(named_ci)$data[[1]]$ymax)
})
test_that("a plot with only an outcome implies the grand mean, drawn as a vline", {
p <- gf_histogram(~Thumb, data = Fingers, binwidth = 5) %>% gf_model()
b <- built(p)
# the inverted geom table would draw an hline where a vline belongs here --
# the outcome is on x, so the mark must be a vline
expect_equal(nrow(b), 1L)
expect_equal(b$xintercept, mean(Fingers$Thumb))
expect_false("yintercept" %in% names(b))
})
test_that("a faceted continuous plot fits each panel's own line", {
p <- gf_point(Thumb ~ Height | Sex, data = Fingers) %>% gf_model()
b <- built(p)
# a precomputed whole-data grid replicated per panel would give both panels
# the same slope
slopes <- vapply(split(b, b$PANEL), function(sub) unname(coef(lm(y ~ x, sub))[[2]]), numeric(1))
by_group <- vapply(split(Fingers, Fingers$Sex), function(sub) {
unname(coef(lm(Thumb ~ Height, sub))[[2]])
}, numeric(1))
expect_equal(unname(slopes), unname(by_group), tolerance = 1e-6)
})
test_that("a faceted plot with only an outcome gives each panel its own mean", {
p <- gf_histogram(~Thumb | Sex, data = Fingers, binwidth = 5) %>% gf_model()
b <- built(p)
# compute_group instead of compute_panel in the axis variant would collapse
# or scatter this differently than one row per panel
expect_equal(nrow(b), 2L)
expect_setequal(b$xintercept, unname(tapply(Fingers$Thumb, Fingers$Sex, mean)))
})
test_that("the inferred line is stable across builds of the same plot", {
# gf_point(), not gf_jitter(): gf_model() deliberately does not pin the
# jitter (its position stays unseeded), so a jittered y is never stable
# across builds for a reason unrelated to what this test names
set.seed(1)
p <- gf_point(shuffle(Thumb) ~ Height, data = Fingers)
q <- p %>% gf_model()
first <- ggplot2::ggplot_build(q)$data[[1]]$y
second <- ggplot2::ggplot_build(q)$data[[1]]$y
expect_equal(first, second)
})
test_that("the inferred line is fit from the pinned column, not a fresh shuffle", {
set.seed(1)
p <- gf_jitter(shuffle(Thumb) ~ Height, data = Fingers)
q <- p %>% gf_model()
from_layer <- coef(lm(y ~ x, built(q)))
from_pin <- coef(lm(q$data$.coursekata_pin_y ~ Fingers$Height))
# fitting from a fresh evaluation instead of the pinned column would give a
# slope near zero, not this one
expect_equal(unname(from_layer), unname(from_pin))
expect_gt(cor(ggplot2::ggplot_build(q)$data[[1]]$y, q$data$.coursekata_pin_y), 0.99)
})
test_that("a pinned axis keeps the reader's label while its mapping points at the pin", {
set.seed(1)
p <- gf_point(shuffle(Thumb) ~ Height, data = Fingers)
q <- p %>% gf_model()
spec <- plot_spec(q)
# getting labels and quosures backwards would silently reintroduce a fresh
# shuffle with no error anywhere
expect_equal(q$labels$y, "shuffle(Thumb)")
expect_equal(spec$axes[["y"]], "shuffle(Thumb)")
expect_equal(rlang::as_label(spec$mapping$y), ".coursekata_pin_y")
})
test_that("the caller's own plot is untouched by the pin gf_model() records on its copy", {
set.seed(1)
p <- gf_jitter(shuffle(Thumb) ~ Height, data = Fingers, width = .2)
q <- p %>% gf_model()
# the bug this guards against lives in p$layers[[i]], a ggproto environment
# -- p$data and p$mapping are copy-on-modify and cannot show it, so a test
# that checked those instead would pass while the caller was still mutated
expect_false(".coursekata_pin_y" %in% names(p$layers[[1]]$data))
expect_equal(rlang::as_label(p$layers[[1]]$mapping$y), "shuffle(Thumb)")
expect_false(isTRUE(all.equal(
ggplot2::ggplot_build(p)$data[[1]]$y,
ggplot2::ggplot_build(q)$data[[1]]$y
)))
})
test_that("a pinned plot's tagged layers keep their tags", {
# MUTATION: the pin's reach test treating this package's own `gf_model()`
# line -- which states its own fitted-y mapping, different from the plot's
# -- as a rival drawer of `y` and refusing to infer at all
set.seed(1)
p <- gf_jitter(shuffle(Thumb) ~ Height, data = Fingers) %>%
gf_model(lm(Thumb ~ Height, data = Fingers))
q <- p %>% gf_model()
expect_false(is.na(layer_index(q, "model")))
expect_equal(length(layer_indices(q, "model")), 2L)
})
test_that("a layer restating the plot's own expression is not a rival drawer", {
# MUTATION: comparing layer and plot mappings as whole quosures (env and
# all) instead of as expressions, which refuses even an identical mapping
# rebuilt in its own environment
p <- gf_point(Thumb ~ log(Height), data = Fingers) %>%
gf_model(lm(Thumb ~ log(Height), data = Fingers))
expect_no_error(p %>% gf_model())
})
test_that("a deterministic non-symbol mapping is pinned unconditionally", {
p <- gf_point(Thumb ~ log(Height), data = Fingers)
q <- p %>% gf_model()
expect_true("x" %in% names(plot_pins(q)))
expect_equal(ggplot2::ggplot_build(q)$data[[1]]$x, ggplot2::ggplot_build(p)$data[[1]]$x)
expect_equal(q$labels$x, "log(Height)")
precomputed <- Fingers
precomputed$log_height <- log(precomputed$Height)
reference <- gf_point(Thumb ~ log_height, data = precomputed) %>% gf_model()
# reintroducing a conditional pin would change more than the mapping's
# spelling; pinning a deterministic expression should change nothing else
expect_equal(built(q), built(reference))
})
test_that("the inferred model does not draw an interaction nobody named", {
p <- gf_point(Thumb ~ Height, color = ~Sex, data = Fingers) %>% gf_model()
b <- built(p)
# inheriting the plot's aesthetics would draw one line per color instead of
# one model of the two axes
expect_equal(length(unique(b$colour)), 1L)
expect_equal(unique(b$colour), "black")
})
test_that("a categorical outcome is refused in the reader's own words", {
expect_error(gf_bar(~Sex, data = Fingers) %>% gf_model(), "numeric outcome variables")
})
test_that("a mapping drawn two different ways by two layers cannot be inferred from", {
p <- ggplot2::ggplot(Fingers, ggplot2::aes(Height, shuffle(Thumb))) +
ggplot2::geom_point() +
ggplot2::geom_point(ggplot2::aes(y = shuffle(Height)))
# the unreachable-pin case has to land at the call, not surface as a build-time
# error from a stat fit on the wrong thing
expect_error(p %>% gf_model(), "two different ways")
})
test_that("a ggplot2-assembled plot with plain symbol mappings needs no pin and builds", {
p <- ggplot2::ggplot(er) + ggplot2::geom_point(ggplot2::aes(base_anxiety, later_anxiety))
q <- p %>% gf_model()
# over-refusing this plot would contradict gf_resid(), which already works on it
expect_equal(
unname(coef(lm(y ~ x, built(q)))),
unname(coef(lm(later_anxiety ~ base_anxiety, data = er)))
)
})
test_that("a bare call prints help instead of inferring a model from nothing", {
# the `pre` gate must not let implied_model_spec() run without a plot
expect_message(gf_model(), "model")
})
test_that("a non-symbol categorical predictor draws one mark per level", {
p <- gf_point(Thumb ~ factor(Year), data = Fingers) %>% gf_model()
b <- built(p)
expect_equal(unname(b$y), as.numeric(tapply(Fingers$Thumb, Fingers$Year, mean)))
ntile3 <- function(x) as.integer(cut(x, quantile(x, c(0, 1 / 3, 2 / 3, 1)), include.lowest = TRUE))
Fingers$tile <- ntile3(Fingers$Thumb)
p2 <- gf_jitter(Thumb ~ as.factor(ntile3(Thumb)), data = Fingers, width = .1) %>% gf_model()
b2 <- built(p2)
expect_equal(unname(b2$y), as.numeric(tapply(Fingers$Thumb, Fingers$tile, mean)))
})
test_that("two gf_model() calls in one pipe do not re-pin an already-pinned column", {
set.seed(1)
q <- gf_jitter(shuffle(Thumb) ~ Height, data = Fingers) %>% gf_model() %>% gf_model()
idx <- layer_indices(q, "model")
expect_equal(length(idx), 2L)
built_layers <- lapply(idx, function(i) ggplot2::ggplot_build(q)$data[[i]])
expect_equal(built_layers[[1]]$y, built_layers[[2]]$y)
# recording .coursekata_pin_y as the "original" would lose shuffle(Thumb)
# from the axis title on the second call
pins <- plot_pins(q)
expect_equal(names(pins), "y")
expect_equal(rlang::as_label(pins$y), "shuffle(Thumb)")
})
test_that("after_stat() mappings are read as unmapped, not as the value they compute", {
p <- gf_density(~Thumb, data = Fingers) %>% gf_model()
b <- built(p)
# without the after_stat() guard this refuses with a numeric-outcome message
# about the density function itself
expect_equal(nrow(b), 1L)
expect_equal(b$xintercept, mean(Fingers$Thumb))
})
test_that("an inferred line stops at the data, whatever else widened the axis", {
# MUTATION: `fullrange = TRUE` as the stated default, or leaving the
# parameter unstated so nothing records that this is a choice.
model <- lm(Thumb ~ Height, data = Fingers)
observed <- range(Fingers$Height)
base <- gf_point(Thumb ~ Height, data = Fingers)
expect_equal(range(built(base %>% gf_lims(x = c(40, 100)) %>% gf_model())$x), observed)
expect_equal(range(built(base %>% gf_b(model) %>% gf_model())$x), observed)
})
test_that("naming the model does not change how far its line is drawn", {
# MUTATION: one path sizing its grid from the plot's axis while the other
# sizes it from the data. The two build their grids by completely different
# routes -- a stat handed the panel's scale as it draws, and a grid computed
# when the call is made -- so nothing but a test holds them to one answer.
# It was `gf_b()` that first pulled them apart: it widens the axis, and for a
# while only one of the two lines stretched to follow.
model <- lm(Thumb ~ Height, data = Fingers)
base <- gf_point(Thumb ~ Height, data = Fingers)
widenings <- list(
plain = function(p) p,
b = function(p) gf_b(p, model),
lims = function(p) gf_lims(p, x = c(40, 100))
)
for (widen in widenings) {
p <- widen(base)
expect_equal(range(built(p %>% gf_model())$x), range(built(p %>% gf_model(model))$x))
}
})
test_that("`se = TRUE` draws the band, not just the numbers for one", {
# MUTATION: GeomLine instead of GeomSmooth. StatSmooth computes `ymin` and
# `ymax` either way, so a test that checks the computed columns passes under
# the mutation and the reader still sees a bare line. The rendered grob is
# the only place the difference exists: a ribbon and a polyline, which is
# what `gf_lm(interval = "confidence")` draws for the same request.
p <- gf_point(Thumb ~ Height, data = Fingers)
banded <- ggplot2::layer_grob(p %>% gf_model(se = TRUE), 2)[[1]]
reference <- ggplot2::layer_grob(p %>% gf_lm(interval = "confidence"), 2)[[1]]
expect_length(banded$children, length(reference$children))
expect_gt(length(banded$children), 1)
})
test_that("the ordinary line keeps the family's own color, not a smoother's", {
# MUTATION: dropping the stated `colour` after the geom swap. GeomSmooth's
# default is a blue that means "a smoother" in ggplot2's vocabulary; this
# line means "the model", and every other mark in this family draws it in
# GeomLine's neutral.
p <- gf_point(Thumb ~ Height, data = Fingers) %>% gf_model()
expect_equal(unique(built(p)$colour), ggplot2::GeomLine$default_aes$colour)
})
test_that("model layers resolve ggplot2's themed defaults before using them as parameters", {
defaults <- ggplot2::GeomLine$default_aes
withr::defer(ggplot2::update_geom_defaults("line", defaults))
ggplot2::update_geom_defaults("line", NULL)
line <- gf_point(Thumb ~ Height, data = Fingers)
groups <- gf_point(Thumb ~ Sex, data = Fingers)
coloured <- gf_point(Thumb ~ Height, data = Fingers, color = ~Sex)
plots <- list(
gf_model(line), gf_model(groups),
gf_model(groups, lm(Thumb ~ Sex, data = Fingers)),
gf_model(coloured, lm(Thumb ~ Height, data = Fingers))
)
for (p in plots) {
expect_equal(unique(built(p)$colour), ggplot2::get_geom_defaults("line")$colour)
}
})
test_that("a caller's own color wins over the stated default, either spelling", {
# MUTATION: dropping the color/colour normalization in `implied_layer_fun()`.
# `modifyList()` matches names literally, so a default stated as `colour`
# outranks the caller's `color =` and the line silently draws black. The
# `segment` shape has stated `colour` for longer than the `line` shape has,
# and was worse off: measured before the normalization, `color = "firebrick"`
# there warned "Duplicated aesthetics after name standardisation: colour".
line <- gf_point(Thumb ~ Height, data = Fingers)
segment <- gf_jitter(Thumb ~ Sex, data = Fingers, width = .1)
expect_equal(unique(built(line %>% gf_model(color = "firebrick"))$colour), "firebrick")
expect_equal(unique(built(line %>% gf_model(colour = "darkgreen"))$colour), "darkgreen")
expect_no_warning(marked <- built(segment %>% gf_model(color = "firebrick")))
expect_equal(unique(marked$colour), "firebrick")
})
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.