diff --git a/DESCRIPTION b/DESCRIPTION index 5ca8f82a..2203c1d7 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: animint2 Title: Animated Interactive Grammar of Graphics -Version: 2026.9.5 +Version: 2026.9.28 URL: https://animint.github.io/animint2 BugReports: https://github.com/animint/animint2/issues Authors@R: c( diff --git a/NEWS.md b/NEWS.md index 118aab5a..744745d2 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,7 @@ +# Changes in version 2026.9.28 (PR#285) + +- Clearer facet error messages when a facet variable is missing from the data, or when `facet_wrap(. ~ var)` uses invalid notation (issue #168). + # Changes in version 2026.9.5 - geom_text() renders with .text(), instead of setMultilineText(), fixing interactive updates. diff --git a/R/facet-layout.r b/R/facet-layout.r index ef312b4a..13f85b9e 100644 --- a/R/facet-layout.r +++ b/R/facet-layout.r @@ -48,6 +48,20 @@ layout_wrap <- function(data, vars = NULL, nrow = NULL, ncol = NULL, vars <- as.quoted(vars) if (length(vars) == 0) return(layout_null()) + # Notation error only when real vars exist; otherwise layout_base reports missing columns. + if ("." %in% names(vars)) { + real_vars <- names(vars)[names(vars) != "."] + all_data_vars <- unique(unlist(lapply(data, names))) + missing_vars <- setdiff(real_vars, all_data_vars) + if (length(missing_vars) == 0) { + stop(sprintf( + "Invalid facet_wrap formula (. ~ var). Use facet_wrap(~%s) or facet_wrap(\"%s\")", + paste(real_vars, collapse = " + "), + paste(real_vars, collapse = "\", \"") + ), call. = FALSE) + } + } + base <- plyr::unrowname(layout_base(data, vars, drop = drop)) id <- plyr::id(base, drop = TRUE) @@ -93,8 +107,22 @@ layout_base <- function(data, vars = NULL, drop = TRUE) { # Form the base data frame which contains all combinations of facetting # variables that appear in the data - has_all <- unlist(plyr::llply(values, length)) == length(vars) + has_all <- unlist(lapply(values, length)) == length(vars) if (!any(has_all)) { + all_data_vars <- unique(unlist(lapply(data, names))) + missing_vars <- setdiff(names(vars)[names(vars) != "."], all_data_vars) + if (length(missing_vars) > 0) { + stop(sprintf( + "Facet variable%s not found in data: %s\nAvailable columns: %s", + if (length(missing_vars) > 1) "s" else "", + paste(missing_vars, collapse = ", "), + paste(all_data_vars, collapse = ", ") + )) + } + # Reached when all facet variables exist somewhere in the data, but no + # single layer has all of them together. For example, facet_grid(X ~ Y) + # where layer 1 only has column X and layer 2 only has column Y — neither + # layer alone satisfies all facetting variables, so has_all is FALSE for every layer. stop("At least one layer must contain all variables used for facetting") } diff --git a/tests/testthat/test-compiler-ghpages.R b/tests/testthat/test-compiler-ghpages.R index 70ef7412..4af477d8 100644 --- a/tests/testthat/test-compiler-ghpages.R +++ b/tests/testthat/test-compiler-ghpages.R @@ -51,6 +51,28 @@ reset_test_repo <- function(){ gh::gh("POST /orgs/animint-test/repos", name = test_repo_name) Sys.sleep(3) } +clone_test_repo <- function(github_url, new_clone){ + max_attempts <- 5 + wait_seconds <- 3 + last_error <- NULL + for (attempt in seq_len(max_attempts)) { + result <- tryCatch({ + clone <- gert::git_clone(github_url, new_clone) + last_error <- NULL + clone + }, error = function(err){ + last_error <<- err + NULL + }) + if (is.null(last_error)) return(result) + if (!grepl("status code: 404", conditionMessage(last_error), fixed = TRUE)) { + stop(last_error) + } + unlink(new_clone, recursive = TRUE, force = TRUE) + if (attempt < max_attempts) Sys.sleep(wait_seconds) + } + stop(last_error) +} test_that("animint2pages(chromote_sleep_seconds=3) creates Capture.PNG", { reset_test_repo() ## first run of animint2pages creates new data viz. @@ -96,7 +118,7 @@ test_that("animint2pages(chromote_sleep_seconds=NULL) does not create Capture.PN ## clone and add Capture.PNG new_clone <- tempfile() github_url <- paste0("https://github.com/", result_list$owner_repo, ".git") - gert::git_clone(github_url, new_clone) + clone_test_repo(github_url, new_clone) branch_name <- gert::git_branch(new_clone) expect_identical(branch_name, "gh-pages", info=paste(branch_name, "should be gh-pages")) cat("FOO", file=file.path(new_clone, "Capture.PNG")) diff --git a/tests/testthat/test-renderer-facet-error-messages.R b/tests/testthat/test-renderer-facet-error-messages.R new file mode 100644 index 00000000..09805f24 --- /dev/null +++ b/tests/testthat/test-renderer-facet-error-messages.R @@ -0,0 +1,79 @@ +acontext("Facet error messages") +missing_var_error <- paste( + "Facet variable not found in data: NonExistentColumn", + "Available columns: Sepal.Length, Sepal.Width, Petal.Length, Petal.Width, Species", + sep = "\n" +) +missing_var_string_error <- paste( + "Facet variable not found in data: MissingVar", + "Available columns: Sepal.Length, Sepal.Width, Petal.Length, Petal.Width, Species", + sep = "\n" +) +bad_notation_error <- "Invalid facet_wrap formula (. ~ var). Use facet_wrap(~Species) or facet_wrap(\"Species\")" +test_that("facet_wrap string missing variable gives clear error", { + viz <- list( + scatter = ggplot() + + facet_wrap("MissingVar") + + geom_point(aes(Sepal.Length, Petal.Length), data = iris) + ) + expect_error({ + animint2dir(viz) + }, missing_var_string_error, fixed = TRUE) +}) +test_that("facet_wrap formula with missing variable gives clear error", { + viz <- list( + scatter = ggplot() + + facet_wrap(. ~ NonExistentColumn) + + geom_point(aes(Sepal.Length, Petal.Length), data = iris) + ) + expect_error({ + animint2dir(viz) + }, missing_var_error, fixed = TRUE) +}) +test_that("facet_wrap bad notation when variable exists gives clear error", { + viz <- list( + scatter = ggplot() + + facet_wrap(. ~ Species) + + geom_point(aes(Sepal.Length, Petal.Length), data = iris) + ) + expect_error({ + animint2dir(viz) + }, bad_notation_error, fixed = TRUE) +}) +test_that("facet_grid missing variable gives clear error", { + viz <- list( + scatter = ggplot() + + facet_grid(. ~ MissingVar) + + geom_point(aes(Sepal.Length, Petal.Length), data = iris) + ) + expect_error({ + animint2dir(viz) + }, missing_var_string_error, fixed = TRUE) +}) +test_that("facet_wrap tilde notation works", { + viz <- list( + scatter = ggplot() + + facet_wrap(~Species) + + geom_point(aes(Sepal.Length, Petal.Length), data = iris) + ) + info <- animint2dir(viz, open.browser = FALSE) + expect_true(file.exists(file.path(info$out.dir, "index.html"))) +}) +test_that("facet_wrap string notation works", { + viz <- list( + scatter = ggplot() + + facet_wrap("Species") + + geom_point(aes(Sepal.Length, Petal.Length), data = iris) + ) + info <- animint2dir(viz, open.browser = FALSE) + expect_true(file.exists(file.path(info$out.dir, "index.html"))) +}) +test_that("facet_grid formula notation works", { + viz <- list( + scatter = ggplot() + + facet_grid(. ~ Species) + + geom_point(aes(Sepal.Length, Petal.Length), data = iris) + ) + info <- animint2dir(viz, open.browser = FALSE) + expect_true(file.exists(file.path(info$out.dir, "index.html"))) +}) diff --git a/tests/testthat/test-renderer3-knit-print.R b/tests/testthat/test-renderer3-knit-print.R index e732b42c..7b3aca2a 100644 --- a/tests/testthat/test-renderer3-knit-print.R +++ b/tests/testthat/test-renderer3-knit-print.R @@ -181,6 +181,7 @@ click_center <- function(id){ ## https://github.com/rstudio/chromote/issues/32 do.call(remDr$Input$dispatchMouseEvent, L) } + Sys.sleep(1) } djs.init.list <- driverjs_get(html)