From e656d18139eb5115345b22420c1fb3a09aa249ca Mon Sep 17 00:00:00 2001 From: Paul Schmidt Date: Thu, 6 Aug 2026 11:00:18 +0200 Subject: [PATCH 1/2] ggdesplot: add panel.border switch and overloaded ticks (WIP: lattice pending) Add a panel.border=TRUE argument (FALSE omits the panel border and axis lines) and overload ticks= to accept, besides the logical FALSE/TRUE, the string "all" (a break at every integer coordinate, resolved per axis) and a list(x=, y=) for explicit per-axis breaks (each element numeric or "all"; a missing element keeps that axis at the default breaks). Fully implemented and tested in ggdesplot(); desplot() gains the argument in its signature and passes it through when gg=TRUE. The lattice (gg=FALSE) path does not yet honour the overloaded ticks / panel.border - to follow, together with document() and R CMD check. 5 new regression tests in test_ggdesplot_fixes.R; full suite 51 pass, 0 fail. --- R/desplot.R | 17 +++++-- R/ggdesplot.R | 68 +++++++++++++++++++++++---- tests/testthat/test_ggdesplot_fixes.R | 50 ++++++++++++++++++++ 3 files changed, 122 insertions(+), 13 deletions(-) diff --git a/R/desplot.R b/R/desplot.R index fcf0690..490d7f8 100644 --- a/R/desplot.R +++ b/R/desplot.R @@ -129,8 +129,16 @@ RedGrayBlue <- colorRampPalette(c("firebrick", "lightgray", "#375997")) #' @param midpoint Method to find midpoint of the color ribbon. #' One of 'midrange', 'median (default), or a numeric value. #' -#' @param ticks If TRUE, show tick marks along the bottom and left sides. -#' +#' @param ticks Controls the axis ticks and labels. One of: \code{FALSE} +#' (default, no axes), \code{TRUE} (axes with the default "pretty" breaks), +#' \code{"all"} (a break at every integer coordinate, resolved separately for +#' each axis), or a list \code{list(x=, y=)} for explicit per-axis control where +#' each element is a numeric vector of breaks or \code{"all"}. A missing list +#' element leaves that axis at the default breaks. +#' +#' @param panel.border If TRUE (default), draw the panel border and axis lines. +#' If FALSE, omit them for a cleaner field map. +#' #' @param flip If TRUE, vertically flip the image. #' #' @param main Main title. @@ -222,7 +230,7 @@ desplot <- function(data, out1.gpar=list(col="black", lwd=3), out2.gpar=list(col="yellow", lwd=1, lty=1), at, midpoint="median", - ticks=FALSE, flip=FALSE, + ticks=FALSE, panel.border=TRUE, flip=FALSE, main=NULL, xlab, ylab, shorten='abb', show.key=TRUE, @@ -319,7 +327,8 @@ desplot <- function(data, col.regions=col.regions, col.text=col.text, out1.gpar=out1.gpar, out2.gpar=out2.gpar, at=at, midpoint=midpoint, - ticks=ticks, flip=flip, main=main, xlab=xlab, ylab=ylab, + ticks=ticks, panel.border=panel.border, + flip=flip, main=main, xlab=xlab, ylab=ylab, shorten=shorten, show.key=show.key, key.cex=key.cex, cex=cex, strip.cex=strip.cex, subset=subset, ...) diff --git a/R/ggdesplot.R b/R/ggdesplot.R index 25bf8d1..7b1d402 100644 --- a/R/ggdesplot.R +++ b/R/ggdesplot.R @@ -71,7 +71,7 @@ ggdesplot <- function(data, out1.gpar=list(col="black", lwd=3), out2.gpar=list(col="yellow", lwd=1, lty=1), at, midpoint="median", - ticks=FALSE, flip=FALSE, + ticks=FALSE, panel.border=TRUE, flip=FALSE, main=NULL, xlab, ylab, shorten='abb', show.key=TRUE, @@ -191,11 +191,14 @@ ggdesplot <- function(data, y.string <- ff$xy[3] panel.string <- ff$cond[1] + # Resolve the (overloaded) 'ticks' argument into a show flag + per-axis breaks. + tk <- .resolve_ticks(ticks, data[[x.string]], data[[y.string]]) + # If ticks are requested, add axis labels if (missing(xlab)) - xlab <- ifelse(ticks, x.string, "") + xlab <- ifelse(tk$show, x.string, "") if (missing(ylab)) - ylab <- ifelse(ticks, y.string, "") + ylab <- ifelse(tk$show, y.string, "") if(has.col){ data[[col.string]] <- factor(data[[col.string]]) # In case it is numeric @@ -560,12 +563,17 @@ ggdesplot <- function(data, xlab(xlab) + ylab(ylab) + # Axis breaks: honour explicit/"all" breaks from 'ticks'; keep flip via reverse. + if(!is.null(tk$x)) + out <- out + scale_x_continuous(breaks = tk$x) if(flip) - out <- out + scale_y_reverse() - + out <- out + scale_y_reverse(breaks = if(is.null(tk$y)) waiver() else tk$y) + else if(!is.null(tk$y)) + out <- out + scale_y_continuous(breaks = tk$y) + # remove axis ticks and labels - if(!ticks) - out <- out + + if(!tk$show) + out <- out + theme(axis.text.x=element_blank(), axis.text.y=element_blank(), axis.ticks=element_blank()) @@ -580,10 +588,12 @@ ggdesplot <- function(data, # blank theme out <- out + - theme(axis.line = element_line(colour = "black"), # left/bottom border + theme(axis.line = if(panel.border) element_line(colour = "black") # left/bottom border + else element_blank(), panel.grid.major = element_blank(), panel.grid.minor = element_blank(), - panel.border = element_rect(fill = NA, colour = "black"), # top/right + panel.border = if(panel.border) element_rect(fill = NA, colour = "black") # top/right + else element_blank(), panel.background = element_blank(), panel.spacing = unit(0, "lines"), # space between panels strip.text = element_text(size = 11 * strip.cex) @@ -591,3 +601,43 @@ ggdesplot <- function(data, out } + +# Normalize the overloaded 'ticks' argument. +# Accepts one of: +# FALSE / TRUE - logical (backward compatible): FALSE hides the axes, TRUE +# shows them with the default (pretty) breaks. +# "all" - show axes with a break at every integer coordinate, resolved +# separately for each axis. +# list(x=, y=) - explicit per-axis control; each element is a numeric vector of +# breaks or "all". A missing element leaves that axis at the +# default (pretty) breaks. +# 'xvals'/'yvals' are the numeric coordinate values, used to resolve "all". +# Returns list(show = , x = , y = ), +# where a NULL break vector means "use the default breaks for that axis". +.resolve_ticks <- function(ticks, xvals, yvals) { + all_int <- function(v) { + v <- v[is.finite(v)] + if(length(v) == 0) return(numeric(0)) + seq(floor(min(v)), ceiling(max(v)), by = 1) + } + one <- function(spec, vals) { + if(is.null(spec)) return(NULL) + if(identical(spec, "all")) return(all_int(vals)) + if(is.numeric(spec)) return(spec) + stop("'ticks' list elements must be numeric or \"all\".", call. = FALSE) + } + if(is.logical(ticks)) { + if(length(ticks) != 1L || is.na(ticks)) + stop("'ticks' must be TRUE, FALSE, \"all\", or a list(x=, y=).", call. = FALSE) + return(list(show = ticks, x = NULL, y = NULL)) + } + if(is.character(ticks) && length(ticks) == 1L && ticks == "all") + return(list(show = TRUE, x = all_int(xvals), y = all_int(yvals))) + if(is.list(ticks)) { + bad <- setdiff(names(ticks), c("x", "y")) + if(is.null(names(ticks)) || length(bad) > 0) + stop("'ticks' list may only have named elements 'x' and 'y'.", call. = FALSE) + return(list(show = TRUE, x = one(ticks$x, xvals), y = one(ticks$y, yvals))) + } + stop("'ticks' must be TRUE, FALSE, \"all\", or a list(x=, y=).", call. = FALSE) +} diff --git a/tests/testthat/test_ggdesplot_fixes.R b/tests/testthat/test_ggdesplot_fixes.R index 7a9df80..e188c19 100644 --- a/tests/testthat/test_ggdesplot_fixes.R +++ b/tests/testthat/test_ggdesplot_fixes.R @@ -15,6 +15,13 @@ partial <- data.frame( rep = factor(paste0("R", rep(1:4, length.out = 16))) ) +# 6 columns x 4 rows: x- and y-range differ, so "all" resolves per-axis and +# 'pretty' (2,4,6) is distinguishable from 'all' (1:6). +fld <- expand.grid(COLUMN = 1:6, ROW = 1:4) +fld$ENTRY <- factor(seq_len(nrow(fld)) %% 12 + 1) +xbreaks <- function(p) { b <- ggplot2::ggplot_build(p)$layout$panel_params[[1]]$x$breaks; b[!is.na(b)] } +ybreaks <- function(p) { b <- ggplot2::ggplot_build(p)$layout$panel_params[[1]]$y$breaks; b[!is.na(b)] } + test_that("ggdesplot does not add a spurious colour legend without 'col'", { skip_if_not(utils::packageVersion("ggplot2") >= "3.5.0") data(besag.met, package = "agridat") @@ -64,3 +71,46 @@ test_that("ggdesplot leaves a cell with a missing value empty", { expect_equal(sum(fill_fac == "transparent"), 1L) expect_equal(length(unique(fill_fac[fill_fac != "transparent"])), 2L) }) + +test_that("ggdesplot panel.border switch toggles the panel border and axis line", { + p_on <- suppressWarnings(ggdesplot(fld, ENTRY ~ COLUMN * ROW)) + p_off <- suppressWarnings(ggdesplot(fld, ENTRY ~ COLUMN * ROW, panel.border = FALSE)) + expect_true(inherits(p_on$theme$panel.border, "element_rect")) # default keeps it + expect_true(inherits(p_off$theme$panel.border, "element_blank")) # switch removes it + expect_true(inherits(p_off$theme$axis.line, "element_blank")) # axis.line follows +}) + +test_that("ggdesplot ticks='all' puts a break at every integer, resolved per axis", { + p <- suppressWarnings(ggdesplot(fld, ENTRY ~ COLUMN * ROW, ticks = "all")) + expect_equal(xbreaks(p), as.numeric(1:6)) + expect_equal(ybreaks(p), as.numeric(1:4)) + expect_false(inherits(p$theme$axis.text.x, "element_blank")) # axes shown + # flip keeps the same data-space breaks (scale_y_reverse negates the positions) + p_flip <- suppressWarnings(ggdesplot(fld, ENTRY ~ COLUMN * ROW, ticks = "all", flip = TRUE)) + expect_true(setequal(abs(ybreaks(p_flip)), 1:4)) +}) + +test_that("ggdesplot ticks=list gives explicit per-axis breaks", { + p <- suppressWarnings(ggdesplot(fld, ENTRY ~ COLUMN * ROW, ticks = list(x = c(1, 3, 5), y = "all"))) + expect_equal(xbreaks(p), c(1, 3, 5)) + expect_equal(ybreaks(p), as.numeric(1:4)) + # a missing list element leaves that axis at the default (pretty) breaks, axes still shown + p2 <- suppressWarnings(ggdesplot(fld, ENTRY ~ COLUMN * ROW, ticks = list(x = c(2, 4)))) + expect_equal(xbreaks(p2), c(2, 4)) + expect_equal(ybreaks(p2), ybreaks(suppressWarnings(ggdesplot(fld, ENTRY ~ COLUMN * ROW, ticks = TRUE)))) + expect_false(inherits(p2$theme$axis.text.x, "element_blank")) +}) + +test_that("ggdesplot logical ticks stay backward compatible", { + p_f <- suppressWarnings(ggdesplot(fld, ENTRY ~ COLUMN * ROW, ticks = FALSE)) + p_t <- suppressWarnings(ggdesplot(fld, ENTRY ~ COLUMN * ROW, ticks = TRUE)) + expect_true(inherits(p_f$theme$axis.text.x, "element_blank")) # FALSE hides axes + expect_false(inherits(p_t$theme$axis.text.x, "element_blank")) # TRUE shows them + expect_false(identical(xbreaks(p_t), as.numeric(1:6))) # pretty (2,4,6), not 'all' +}) + +test_that("ggdesplot rejects an invalid ticks specification", { + expect_error(suppressWarnings(ggdesplot(fld, ENTRY ~ COLUMN * ROW, ticks = "foo"))) + expect_error(suppressWarnings(ggdesplot(fld, ENTRY ~ COLUMN * ROW, ticks = list(z = 1)))) + expect_error(suppressWarnings(ggdesplot(fld, ENTRY ~ COLUMN * ROW, ticks = NA))) +}) From fec31a8f2d67ece1fb31ac4999927f7caf3e801f Mon Sep 17 00:00:00 2001 From: Paul Schmidt Date: Thu, 6 Aug 2026 12:18:31 +0200 Subject: [PATCH 2/2] desplot: honour ticks and panel.border in the lattice path The overloaded 'ticks' (FALSE/TRUE/"all"/list(x=,y=)) and the new 'panel.border' switch were only wired into ggdesplot(); the lattice path ignored them. Resolve 'ticks' via .resolve_ticks() into the scales' draw/at, and drop the panel box + axis lines for panel.border=FALSE by merging axis.line into any par.settings passed through '...'. Import stats::update. Adds docs, NEWS, and lattice regression tests. --- NAMESPACE | 1 + NEWS.md | 4 ++ R/desplot.R | 56 +++++++++++++++++++-------- man/desplot.Rd | 19 +++++++-- tests/testthat/test_ggdesplot_fixes.R | 54 ++++++++++++++++++++++++++ 5 files changed, 115 insertions(+), 19 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 0e30a94..69d7123 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -17,3 +17,4 @@ importFrom(rlang,.data) importFrom(stats,as.formula) importFrom(stats,formula) importFrom(stats,median) +importFrom(stats,update) diff --git a/NEWS.md b/NEWS.md index c24bf83..63eef9d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # desplot 1.11 () +* `ticks` now accepts `"all"` (a break at every integer coordinate) or a `list(x=, y=)` for explicit per-axis breaks, in both `desplot()` and `ggdesplot()`. `TRUE`/`FALSE` behave as before. (P.Schmidt) + +* New argument `panel.border` (default `TRUE`) toggles the panel border and axis lines, in both `desplot()` and `ggdesplot()`. Set `FALSE` for a cleaner field map. (P.Schmidt) + * `ggdesplot()` now leaves cells with a missing value empty, as `desplot()` does. Previously they were filled with the ggplot2 default grey, which lies inside the range of the default red-gray-blue scale and so looked like a mid-range value. (P.Schmidt) * `ggdesplot()` no longer draws a spurious `no_color` legend when `text` or `num` is used without `col`. (P.Schmidt) diff --git a/R/desplot.R b/R/desplot.R index 490d7f8..7d86287 100644 --- a/R/desplot.R +++ b/R/desplot.R @@ -183,7 +183,7 @@ RedGrayBlue <- colorRampPalette(c("firebrick", "lightgray", "#375997")) #' @import grid #' @import lattice #' @importFrom reshape2 acast melt -#' @importFrom stats as.formula formula median +#' @importFrom stats as.formula formula median update #' @export #' @rdname desplot #' @@ -212,13 +212,16 @@ RedGrayBlue <- colorRampPalette(c("firebrick", "lightgray", "#375997")) #' yield ~ col+row, #' out1=block, out2=gen, aspect=28.4/44) #' -#' desplot(yates.oats, -#' block ~ col+row, +#' desplot(yates.oats, +#' block ~ col+row, #' col=nitro, text=gen, cex=1, out1=block, #' out2=gen, out2.gpar=list(col = "gray50", lwd = 1, lty = 1)) -#' +#' +#' # Overloaded 'ticks' (a break at every integer) and the 'panel.border' switch +#' desplot(yates.oats, yield ~ col+row, ticks="all", panel.border=FALSE) +#' #' } -desplot <- function(data, +desplot <- function(data, form=formula(NULL ~ x + y), num=NULL, num.string=NULL, col=NULL, col.string=NULL, @@ -369,12 +372,6 @@ desplot <- function(data, y.string <- ff$xy[3] panel.string <- ff$cond[1] - # If ticks are requested, add axis labels - if (missing(xlab)) - xlab <- ifelse(ticks, x.string, "") - if (missing(ylab)) - ylab <- ifelse(ticks, y.string, "") - # Determine what fills the cells: nothing, character/factor, or numeric if(is.null(fill.string)) fill.type="none" else if (is.factor(data[[fill.string]])) @@ -510,9 +507,19 @@ desplot <- function(data, fac2num <- function(x) as.numeric(levels(x))[x] if(is.factor(data[[x.string]])) data[[x.string]] <- fac2num(data[[x.string]]) - if(is.factor(data[[y.string]])) + if(is.factor(data[[y.string]])) data[[y.string]] <- fac2num(data[[y.string]]) + # Resolve the (overloaded) 'ticks' argument into a show flag + per-axis breaks. + # Done after x/y are numeric so "all" can enumerate the integer coordinates. + tk <- .resolve_ticks(ticks, data[[x.string]], data[[y.string]]) + + # If ticks are requested, add axis labels + if (missing(xlab)) + xlab <- ifelse(tk$show, x.string, "") + if (missing(ylab)) + ylab <- ifelse(tk$show, y.string, "") + # Check for multiple values for each cell. if(is.null(ff$cond)) { # no factor for panels @@ -725,7 +732,13 @@ desplot <- function(data, out1.val <- if(has.out1) data[[out1.string]] else NULL out2.val <- if(has.out2) data[[out2.string]] else NULL - + + # Assemble the axis scales from the resolved 'ticks'. A NULL break vector + # (tk$x / tk$y) leaves that axis at lattice's default (pretty) breaks. + scales.arg <- list(relation = "free", draw = tk$show) + if(!is.null(tk$x)) scales.arg$x <- list(at = tk$x) + if(!is.null(tk$y)) scales.arg$y <- list(at = tk$y) + out <- levelplot(form, data=data, @@ -741,9 +754,7 @@ desplot <- function(data, main=main, xlab=xlab, ylab=ylab, - scales=list(relation='free', # Different scales for each panel - draw=ticks # Don't draw panel axes - ), + scales=scales.arg, prepanel = prepanel.desplot, panel=function(x, y, z, subscripts, groups, ..., out1f, out1g, out2f, out2g, dq){ @@ -762,6 +773,19 @@ desplot <- function(data, }, strip=strip.custom(par.strip.text=list(cex=strip.cex)), ...) + # panel.border=FALSE: drop the panel box + axis lines that lattice draws by + # default (the ggplot2 version does the same). Merge into any par.settings the + # user passed through '...' rather than adding a second par.settings argument. + if(!panel.border) { + ps <- out$par.settings + if(is.null(ps)) ps <- list() + al <- ps$axis.line + if(is.null(al)) al <- list() + al$col <- "transparent" + ps$axis.line <- al + out <- update(out, par.settings = ps) + } + # Use 'update' for any other modifications #if(!show.key) out <- update(out, legend=list(left=NULL)) diff --git a/man/desplot.Rd b/man/desplot.Rd index 02d7440..f6baf31 100644 --- a/man/desplot.Rd +++ b/man/desplot.Rd @@ -28,6 +28,7 @@ desplot( at, midpoint = "median", ticks = FALSE, + panel.border = TRUE, flip = FALSE, main = NULL, xlab, @@ -66,6 +67,7 @@ ggdesplot( at, midpoint = "median", ticks = FALSE, + panel.border = TRUE, flip = FALSE, main = NULL, xlab, @@ -143,7 +145,15 @@ Note: using 'at' causes 'midpoint' to be set to NULL.} \item{midpoint}{Method to find midpoint of the color ribbon. One of 'midrange', 'median (default), or a numeric value.} -\item{ticks}{If TRUE, show tick marks along the bottom and left sides.} +\item{ticks}{Controls the axis ticks and labels. One of: \code{FALSE} +(default, no axes), \code{TRUE} (axes with the default "pretty" breaks), +\code{"all"} (a break at every integer coordinate, resolved separately for +each axis), or a list \code{list(x=, y=)} for explicit per-axis control where +each element is a numeric vector of breaks or \code{"all"}. A missing list +element leaves that axis at the default breaks.} + +\item{panel.border}{If TRUE (default), draw the panel border and axis lines. +If FALSE, omit them for a cleaner field map.} \item{flip}{If TRUE, vertically flip the image.} @@ -250,11 +260,14 @@ ggdesplot(yates.oats, yield ~ col+row, out1=block, out2=gen, aspect=28.4/44) -desplot(yates.oats, - block ~ col+row, +desplot(yates.oats, + block ~ col+row, col=nitro, text=gen, cex=1, out1=block, out2=gen, out2.gpar=list(col = "gray50", lwd = 1, lty = 1)) +# Overloaded 'ticks' (a break at every integer) and the 'panel.border' switch +desplot(yates.oats, yield ~ col+row, ticks="all", panel.border=FALSE) + } } \references{ diff --git a/tests/testthat/test_ggdesplot_fixes.R b/tests/testthat/test_ggdesplot_fixes.R index e188c19..5927156 100644 --- a/tests/testthat/test_ggdesplot_fixes.R +++ b/tests/testthat/test_ggdesplot_fixes.R @@ -114,3 +114,57 @@ test_that("ggdesplot rejects an invalid ticks specification", { expect_error(suppressWarnings(ggdesplot(fld, ENTRY ~ COLUMN * ROW, ticks = list(z = 1)))) expect_error(suppressWarnings(ggdesplot(fld, ENTRY ~ COLUMN * ROW, ticks = NA))) }) + +# --------------------------------------------------------------------------- +# Same 'ticks' / 'panel.border' features on the lattice path, so desplot() and +# ggdesplot() behave identically. The lattice trellis object stores the axis +# spec in p$x.scales$draw / $at and the panel box colour in +# p$par.settings$axis.line$col. +# --------------------------------------------------------------------------- + +test_that("desplot ticks='all' puts a break at every integer, resolved per axis", { + p <- desplot(fld, ENTRY ~ COLUMN * ROW, ticks = "all") + expect_true(isTRUE(p$x.scales$draw)) + expect_equal(as.numeric(p$x.scales$at), as.numeric(1:6)) + expect_equal(as.numeric(p$y.scales$at), as.numeric(1:4)) + # flip keeps the same break values and does not error while building + p_flip <- desplot(fld, ENTRY ~ COLUMN * ROW, ticks = "all", flip = TRUE) + expect_equal(as.numeric(p_flip$y.scales$at), as.numeric(1:4)) +}) + +test_that("desplot ticks=list gives explicit per-axis breaks", { + p <- desplot(fld, ENTRY ~ COLUMN * ROW, ticks = list(x = c(1, 3, 5), y = "all")) + expect_equal(as.numeric(p$x.scales$at), c(1, 3, 5)) + expect_equal(as.numeric(p$y.scales$at), as.numeric(1:4)) + # a missing list element leaves that axis at lattice's default (at = FALSE) + p2 <- desplot(fld, ENTRY ~ COLUMN * ROW, ticks = list(x = c(2, 4))) + expect_equal(as.numeric(p2$x.scales$at), c(2, 4)) + expect_identical(p2$y.scales$at, FALSE) + expect_true(isTRUE(p2$x.scales$draw)) +}) + +test_that("desplot logical ticks stay backward compatible", { + p_f <- desplot(fld, ENTRY ~ COLUMN * ROW, ticks = FALSE) + p_t <- desplot(fld, ENTRY ~ COLUMN * ROW, ticks = TRUE) + expect_true(isFALSE(p_f$x.scales$draw)) # FALSE hides axes + expect_true(isTRUE(p_t$x.scales$draw)) # TRUE shows them + expect_false(identical(as.numeric(p_t$x.scales$at), as.numeric(1:6))) # default breaks, not 'all' +}) + +test_that("desplot panel.border switch toggles the panel box and axis line", { + p_on <- desplot(fld, ENTRY ~ COLUMN * ROW) + p_off <- desplot(fld, ENTRY ~ COLUMN * ROW, panel.border = FALSE) + expect_false(identical(p_on$par.settings$axis.line$col, "transparent")) # default keeps it + expect_identical(p_off$par.settings$axis.line$col, "transparent") # switch removes it + # a par.settings the user passes through '...' survives the merge + p_mix <- desplot(fld, ENTRY ~ COLUMN * ROW, panel.border = FALSE, + par.settings = list(strip.background = list(col = "grey90"))) + expect_identical(p_mix$par.settings$axis.line$col, "transparent") + expect_identical(p_mix$par.settings$strip.background$col, "grey90") +}) + +test_that("desplot rejects an invalid ticks specification", { + expect_error(desplot(fld, ENTRY ~ COLUMN * ROW, ticks = "foo")) + expect_error(desplot(fld, ENTRY ~ COLUMN * ROW, ticks = list(z = 1))) + expect_error(desplot(fld, ENTRY ~ COLUMN * ROW, ticks = NA)) +})