From bea908944966b92df6335bdf8c9dc5db77d7cf19 Mon Sep 17 00:00:00 2001 From: Teun van den Brand <49372158+teunbrand@users.noreply.github.com> Date: Fri, 24 Nov 2023 13:04:03 +0100 Subject: [PATCH] Axis guide for logarithmic ticks (#5500) * fix recycle bug * set default minor.ticks in axis * Draft guide * Better censoring in symmetric scales * internally cast args to `rel()` * change mirror strategy * interpret numeric as `rel()` * warn when prescale_base and scale transform are set * add control over whether to use expanded range * negative_small cannot be 0 or negative * capping works with new ticks * Add tests * Document * declare trans as function rather than strings * Add pkgdown item * Enable theming for short ticks * Mark `annotation_logticks()` as superseded * add news bullet --- DESCRIPTION | 1 + NAMESPACE | 2 + NEWS.md | 4 + R/annotation-logticks.R | 5 + R/guide-.R | 2 +- R/guide-axis-logticks.R | 264 ++++++++++++++++++ R/guide-axis.R | 2 +- _pkgdown.yml | 1 + man/annotation_logticks.Rd | 4 + man/ggplot2-ggproto.Rd | 13 +- man/guide_axis_logticks.Rd | 115 ++++++++ tests/testthat/_snaps/guides.md | 4 + .../logtick-axes-with-customisation.svg | 206 ++++++++++++++ tests/testthat/test-guides.R | 100 +++++++ 14 files changed, 716 insertions(+), 7 deletions(-) create mode 100644 R/guide-axis-logticks.R create mode 100644 man/guide_axis_logticks.Rd create mode 100644 tests/testthat/_snaps/guides/logtick-axes-with-customisation.svg diff --git a/DESCRIPTION b/DESCRIPTION index 77cb1ce98d..1b869d4bdf 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -175,6 +175,7 @@ Collate: 'grouping.R' 'guide-.R' 'guide-axis.R' + 'guide-axis-logticks.R' 'guide-axis-theta.R' 'guide-legend.R' 'guide-bins.R' diff --git a/NAMESPACE b/NAMESPACE index c5f5a94219..44cb42fceb 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -212,6 +212,7 @@ export(GeomViolin) export(GeomVline) export(Guide) export(GuideAxis) +export(GuideAxisLogticks) export(GuideBins) export(GuideColourbar) export(GuideColoursteps) @@ -420,6 +421,7 @@ export(ggproto_parent) export(ggsave) export(ggtitle) export(guide_axis) +export(guide_axis_logticks) export(guide_axis_theta) export(guide_bins) export(guide_colorbar) diff --git a/NEWS.md b/NEWS.md index a7bd1b024e..bea2948661 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # ggplot2 (development version) +* New `guide_axis_logticks()` can be used to draw logarithmic tick marks as + an axis. It supersedes the `annotation_logticks()` function + (@teunbrand, #5325). + * Glyphs drawing functions of the `draw_key_*()` family can now set `"width"` and `"height"` attributes (in centimetres) to the produced keys to control their displayed size in the legend. diff --git a/R/annotation-logticks.R b/R/annotation-logticks.R index 1e7f60be65..8f3e8a63c2 100644 --- a/R/annotation-logticks.R +++ b/R/annotation-logticks.R @@ -1,5 +1,10 @@ #' Annotation: log tick marks #' +#' @description +#' `r lifecycle::badge("superseded")` +#' +#' This function is superseded by using [`guide_axis_logticks()`]. +#' #' This annotation adds log tick marks with diminishing spacing. #' These tick marks probably make sense only for base 10. #' diff --git a/R/guide-.R b/R/guide-.R index daf026e88a..cdb750ce56 100644 --- a/R/guide-.R +++ b/R/guide-.R @@ -391,7 +391,7 @@ Guide <- ggproto( pos <- unname(c(top = 1, bottom = 0, left = 0, right = 1)[position]) dir <- -2 * pos + 1 pos <- unit(rep(pos, 2 * n_breaks), "npc") - dir <- rep(vec_interleave(dir, 0), n_breaks) * tick_len + dir <- rep(vec_interleave(dir, 0), n_breaks) * rep(tick_len, each = 2) tick <- pos + dir # Build grob diff --git a/R/guide-axis-logticks.R b/R/guide-axis-logticks.R new file mode 100644 index 0000000000..5e97d3f193 --- /dev/null +++ b/R/guide-axis-logticks.R @@ -0,0 +1,264 @@ +#' @include guide-axis.R +NULL + +#' Axis with logarithmic tick marks +#' +#' This axis guide replaces the placement of ticks marks at intervals in +#' log10 space. +#' +#' @param long,mid,short A [grid::unit()] object or [rel()] object setting +#' the (relative) length of the long, middle and short ticks. Numeric values +#' are interpreted as [rel()] objects. The [rel()] values are used to multiply +#' values of the `axis.ticks.length` theme setting. +#' @param prescale_base Base of logarithm used to transform data manually. The +#' default, `NULL`, will use the scale transformation to calculate positions. +#' Only set `prescale_base` if the data has already been log-transformed. +#' When using a log-transform in the position scale or in `coord_trans()`, +#' keep the default `NULL` argument. +#' @param negative_small When the scale limits include 0 or negative numbers, +#' what should be the smallest absolute value that is marked with a tick? +#' @param short_theme A theme [element][element_line()] for customising the +#' display of the shortest ticks. Must be a line or blank element, and +#' it inherits from the `axis.minor.ticks` setting for the relevant position. +#' @param expanded Whether the ticks should cover the range after scale +#' expansion (`TRUE`, default), or be restricted to the scale limits +#' (`FALSE`). +#' @inheritParams guide_axis +#' @inheritDotParams guide_axis -minor.ticks +#' +#' @export +#' +#' @examples +#' # A standard plot +#' p <- ggplot(msleep, aes(bodywt, brainwt)) + +#' geom_point(na.rm = TRUE) +#' +#' # The logticks axis works well with log scales +#' p + scale_x_log10(guide = "axis_logticks") + +#' scale_y_log10(guide = "axis_logticks") +#' +#' # Or with log-transformed coordinates +#' p + coord_trans(x = "log10", y = "log10") + +#' guides(x = "axis_logticks", y = "axis_logticks") +#' +#' # When data is transformed manually, one should provide `prescale_base` +#' # Keep in mind that this axis uses log10 space for placement, not log2 +#' p + aes(x = log2(bodywt), y = log10(brainwt)) + +#' guides( +#' x = guide_axis_logticks(prescale_base = 2), +#' y = guide_axis_logticks(prescale_base = 10) +#' ) +#' +#' # A plot with both positive and negative extremes, pseudo-log transformed +#' set.seed(42) +#' p2 <- ggplot(data.frame(x = rcauchy(1000)), aes(x = x)) + +#' geom_density() + +#' scale_x_continuous( +#' breaks = c(-10^(4:0), 0, 10^(0:4)), +#' trans = "pseudo_log" +#' ) +#' +#' # The log ticks are mirrored when 0 is included +#' p2 + guides(x = "axis_logticks") +#' +#' # To control the tick density around 0, one can set `negative_small` +#' p2 + guides(x = guide_axis_logticks(negative_small = 1)) +guide_axis_logticks <- function( + long = 2.25, + mid = 1.5, + short = 0.75, + prescale_base = NULL, + negative_small = 0.1, + short_theme = element_line(), + expanded = TRUE, + cap = "none", + ... +) { + if (is.logical(cap)) { + check_bool(cap) + cap <- if (cap) "both" else "none" + } + cap <- arg_match0(cap, c("none", "both", "upper", "lower")) + + if (is_bare_numeric(long)) long <- rel(long) + if (is_bare_numeric(mid)) mid <- rel(mid) + if (is_bare_numeric(short)) short <- rel(short) + + check_fun <- function(x) (is.rel(x) || is.unit(x)) && length(x) == 1 + what <- "a {.cls rel} or {.cls unit} object of length 1" + check_object(long, check_fun, what) + check_object(mid, check_fun, what) + check_object(short, check_fun, what) + check_number_decimal( + negative_small, min = 1e-100, # minimal domain of scales::log_trans + allow_infinite = FALSE, + allow_null = TRUE + ) + check_bool(expanded) + check_inherits(short_theme, c("element_blank", "element_line")) + + new_guide( + available_aes = c("x", "y"), + prescale_base = prescale_base, + negative_small = negative_small, + expanded = expanded, + long = long, + mid = mid, + short = short, + cap = cap, + minor.ticks = TRUE, + short_theme = short_theme, + ..., + super = GuideAxisLogticks + ) +} + +#' @rdname ggplot2-ggproto +#' @format NULL +#' @usage NULL +#' @export +GuideAxisLogticks <- ggproto( + "GuideAxisLogticks", GuideAxis, + + params = defaults( + list( + prescale_base = NULL, + negative_small = 0.1, + minor.ticks = TRUE, # for spacing calculation + long = 2.25, + mid = 1.5, + short = 0.75, + expanded = TRUE, + short_theme = NULL + ), + GuideAxis$params + ), + + # Here we calculate a 'shadow key' that only applies to the tickmarks. + extract_params = function(scale, params, ...) { + + if (scale$is_discrete()) { + cli::cli_abort("Cannot calculate logarithmic ticks for discrete scales.") + } + + aesthetic <- params$aesthetic + params$name <- paste0(params$name, "_", aesthetic) + params + + # Reconstruct a transformation if user has prescaled data + if (!is.null(params$prescale_base)) { + trans_name <- scale$scale$trans$name + if (trans_name != "identity") { + cli::cli_warn(paste0( + "The {.arg prescale_base} argument will override the scale's ", + "{.field {trans_name}} transformation in log-tick positioning." + )) + } + trans <- log_trans(base = params$prescale_base) + } else { + trans <- scale$scale$trans + } + + # Reconstruct original range + limits <- trans$inverse(scale$get_limits()) + has_negatives <- any(limits <= 0) + + if (!has_negatives) { + start <- floor(log10(min(limits))) - 1L + end <- ceiling(log10(max(limits))) + 1L + } else { + params$negative_small <- params$negative_small %||% 0.1 + start <- floor(log10(abs(params$negative_small))) + end <- ceiling(log10(max(abs(limits)))) + 1L + } + + # Calculate tick marks + tens <- 10^seq(start, end, by = 1) + fives <- tens * 5 + ones <- as.vector(outer(setdiff(2:9, 5), tens)) + + if (has_negatives) { + # Filter and mirror ticks around 0 + tens <- tens[tens >= params$negative_small] + tens <- c(tens, -tens, 0) + fives <- fives[fives >= params$negative_small] + fives <- c(fives, -fives) + ones <- ones[ones >= params$negative_small] + ones <- c(ones, -ones) + } + + # Set ticks back into transformed space + ticks <- trans$transform(c(tens, fives, ones)) + nticks <- c(length(tens), length(fives), length(ones)) + + logkey <- data_frame0( + !!aesthetic := ticks, + .type = rep(1:3, times = nticks) + ) + + # Discard out-of-bounds ticks + range <- if (params$expanded) scale$continuous_range else scale$get_limits() + logkey <- vec_slice(logkey, ticks >= range[1] & ticks <= range[2]) + + # Adjust capping based on these ticks instead of regular ticks + if (params$cap %in% c("both", "upper")) { + params$decor[[aesthetic]][2] <- max(logkey[[aesthetic]]) + } + if (params$cap %in% c("both", "lower")) { + params$decor[[aesthetic]][1] <- min(logkey[[aesthetic]]) + } + + params$logkey <- logkey + params + }, + + transform = function(self, params, coord, panel_params) { + params <- GuideAxis$transform(params, coord, panel_params) + # Also transform the logkey + params$logkey <- coord$transform(params$logkey, panel_params) + params + }, + + override_elements = function(params, elements, theme) { + elements <- GuideAxis$override_elements(params, elements, theme) + length <- elements$major_length + + # Inherit short ticks from minor ticks + elements$short <- combine_elements(params$short_theme, elements$minor) + + # Multiply rel units with theme's tick length + tick_length <- lapply(params[c("long", "mid", "short")], function(x) { + if (is.unit(x)) x else unclass(x) * length + }) + tick_length <- inject(unit.c(!!!tick_length)) + elements$tick_length <- tick_length + + # We replace the lengths so that spacing calculation works out as intended + elements$major_length <- max(tick_length) + elements$minor_length <- min(tick_length) + elements + }, + + build_ticks = function(key, elements, params, position = params$opposite) { + # Instead of passing regular key, we pass the logkey + key <- params$logkey + long <- Guide$build_ticks( + vec_slice(key, key$.type == 1L), + elements$ticks, params, position, + elements$tick_length[1L] + ) + + mid <- Guide$build_ticks( + vec_slice(key, key$.type == 2L), + elements$minor, params, position, + elements$tick_length[2L] + ) + + short <- Guide$build_ticks( + vec_slice(key, key$.type == 3L), + elements$short, params, position, + elements$tick_length[3L] + ) + grobTree(long, mid, short, name = "ticks") + } +) diff --git a/R/guide-axis.R b/R/guide-axis.R index de3ffb4a6a..0e8e49215c 100644 --- a/R/guide-axis.R +++ b/R/guide-axis.R @@ -111,7 +111,7 @@ GuideAxis <- ggproto( minor_length = "axis.minor.ticks.length" ), - extract_key = function(scale, aesthetic, minor.ticks, ...) { + extract_key = function(scale, aesthetic, minor.ticks = FALSE, ...) { major <- Guide$extract_key(scale, aesthetic, ...) if (!minor.ticks) { return(major) diff --git a/_pkgdown.yml b/_pkgdown.yml index fa854d76ef..1bbe6e33ef 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -126,6 +126,7 @@ reference: - guide_colourbar - guide_legend - guide_axis + - guide_axis_logticks - guide_axis_theta - guide_bins - guide_coloursteps diff --git a/man/annotation_logticks.Rd b/man/annotation_logticks.Rd index 92a587e708..490a7d3b17 100644 --- a/man/annotation_logticks.Rd +++ b/man/annotation_logticks.Rd @@ -61,6 +61,10 @@ long tick marks. In base 10, these are the "1" (or "10") ticks.} \item{size}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}}} } \description{ +\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#superseded}{\figure{lifecycle-superseded.svg}{options: alt='[Superseded]'}}}{\strong{[Superseded]}} + +This function is superseded by using \code{\link[=guide_axis_logticks]{guide_axis_logticks()}}. + This annotation adds log tick marks with diminishing spacing. These tick marks probably make sense only for base 10. } diff --git a/man/ggplot2-ggproto.Rd b/man/ggplot2-ggproto.Rd index 04e9780bfe..728fcb2410 100644 --- a/man/ggplot2-ggproto.Rd +++ b/man/ggplot2-ggproto.Rd @@ -12,8 +12,8 @@ % R/geom-errorbarh.R, R/geom-function.R, R/geom-hex.R, R/geom-hline.R, % R/geom-label.R, R/geom-linerange.R, R/geom-point.R, R/geom-pointrange.R, % R/geom-quantile.R, R/geom-rug.R, R/geom-smooth.R, R/geom-spoke.R, -% R/geom-text.R, R/geom-tile.R, R/geom-violin.R, R/geom-vline.R, -% R/guide-.R, R/guide-axis.R, R/guide-legend.R, R/guide-bins.R, +% R/geom-text.R, R/geom-tile.R, R/geom-violin.R, R/geom-vline.R, R/guide-.R, +% R/guide-axis.R, R/guide-axis-logticks.R, R/guide-legend.R, R/guide-bins.R, % R/guide-colorbar.R, R/guide-colorsteps.R, R/guide-none.R, R/guide-old.R, % R/layout.R, R/position-.R, R/position-dodge.R, R/position-dodge2.R, % R/position-identity.R, R/position-jitter.R, R/position-jitterdodge.R, @@ -91,6 +91,7 @@ \alias{GeomVline} \alias{Guide} \alias{GuideAxis} +\alias{GuideAxisLogticks} \alias{GuideLegend} \alias{GuideBins} \alias{GuideColourbar} @@ -423,9 +424,11 @@ range. \item \code{merge()} Combines information from multiple guides with the same \code{params$hash}. This ensures that e.g. \code{guide_legend()} can display both \code{shape} and \code{colour} in the same guide. -\item \code{get_layer_key()} Extract information from layers. This can be used to -check that the guide's aesthetic is actually in use, or to gather -information about how legend keys should be displayed. +\item \code{process_layers()} Extract information from layers. This acts mostly +as a filter for which layers to include and these are then (typically) +forwarded to \code{get_layer_key()}. +\item \code{get_layer_key()} This can be used to gather information about how legend +keys should be displayed. \item \code{setup_params()} Set up parameters at the beginning of drawing stages. It can be used to overrule user-supplied parameters or perform checks on the \code{params} property. diff --git a/man/guide_axis_logticks.Rd b/man/guide_axis_logticks.Rd new file mode 100644 index 0000000000..60ebaa8b12 --- /dev/null +++ b/man/guide_axis_logticks.Rd @@ -0,0 +1,115 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guide-axis-logticks.R +\name{guide_axis_logticks} +\alias{guide_axis_logticks} +\title{Axis with logarithmic tick marks} +\usage{ +guide_axis_logticks( + long = 2.25, + mid = 1.5, + short = 0.75, + prescale_base = NULL, + negative_small = 0.1, + short_theme = element_line(), + expanded = TRUE, + cap = "none", + ... +) +} +\arguments{ +\item{long, mid, short}{A \code{\link[grid:unit]{grid::unit()}} object or \code{\link[=rel]{rel()}} object setting +the (relative) length of the long, middle and short ticks. Numeric values +are interpreted as \code{\link[=rel]{rel()}} objects. The \code{\link[=rel]{rel()}} values are used to multiply +values of the \code{axis.ticks.length} theme setting.} + +\item{prescale_base}{Base of logarithm used to transform data manually. The +default, \code{NULL}, will use the scale transformation to calculate positions. +Only set \code{prescale_base} if the data has already been log-transformed. +When using a log-transform in the position scale or in \code{coord_trans()}, +keep the default \code{NULL} argument.} + +\item{negative_small}{When the scale limits include 0 or negative numbers, +what should be the smallest absolute value that is marked with a tick?} + +\item{short_theme}{A theme \link[=element_line]{element} for customising the +display of the shortest ticks. Must be a line or blank element, and +it inherits from the \code{axis.minor.ticks} setting for the relevant position.} + +\item{expanded}{Whether the ticks should cover the range after scale +expansion (\code{TRUE}, default), or be restricted to the scale limits +(\code{FALSE}).} + +\item{cap}{A \code{character} to cut the axis line back to the last breaks. Can +be \code{"none"} (default) to draw the axis line along the whole panel, or +\code{"upper"} and \code{"lower"} to draw the axis to the upper or lower break, or +\code{"both"} to only draw the line in between the most extreme breaks. \code{TRUE} +and \code{FALSE} are shorthand for \code{"both"} and \code{"none"} respectively.} + +\item{...}{ + Arguments passed on to \code{\link[=guide_axis]{guide_axis}} + \describe{ + \item{\code{check.overlap}}{silently remove overlapping labels, +(recursively) prioritizing the first, last, and middle labels.} + \item{\code{angle}}{Compared to setting the angle in \code{\link[=theme]{theme()}} / \code{\link[=element_text]{element_text()}}, +this also uses some heuristics to automatically pick the \code{hjust} and \code{vjust} that +you probably want. Can be one of the following: +\itemize{ +\item \code{NULL} to take the angles and \code{hjust}/\code{vjust} directly from the theme. +\item \code{waiver()} to allow reasonable defaults in special cases. +\item A number representing the text angle in degrees. +}} + \item{\code{n.dodge}}{The number of rows (for vertical axes) or columns (for +horizontal axes) that should be used to render the labels. This is +useful for displaying labels that would otherwise overlap.} + \item{\code{order}}{A positive \code{integer} of length 1 that specifies the order of +this guide among multiple guides. This controls in which order guides are +merged if there are multiple guides for the same position. If 0 (default), +the order is determined by a secret algorithm.} + \item{\code{position}}{Where this guide should be drawn: one of top, bottom, +left, or right.} + \item{\code{title}}{A character string or expression indicating a title of guide. +If \code{NULL}, the title is not shown. By default +(\code{\link[=waiver]{waiver()}}), the name of the scale object or the name +specified in \code{\link[=labs]{labs()}} is used for the title.} + }} +} +\description{ +This axis guide replaces the placement of ticks marks at intervals in +log10 space. +} +\examples{ +# A standard plot +p <- ggplot(msleep, aes(bodywt, brainwt)) + + geom_point(na.rm = TRUE) + +# The logticks axis works well with log scales +p + scale_x_log10(guide = "axis_logticks") + + scale_y_log10(guide = "axis_logticks") + +# Or with log-transformed coordinates +p + coord_trans(x = "log10", y = "log10") + + guides(x = "axis_logticks", y = "axis_logticks") + +# When data is transformed manually, one should provide `prescale_base` +# Keep in mind that this axis uses log10 space for placement, not log2 +p + aes(x = log2(bodywt), y = log10(brainwt)) + + guides( + x = guide_axis_logticks(prescale_base = 2), + y = guide_axis_logticks(prescale_base = 10) + ) + +# A plot with both positive and negative extremes, pseudo-log transformed +set.seed(42) +p2 <- ggplot(data.frame(x = rcauchy(1000)), aes(x = x)) + + geom_density() + + scale_x_continuous( + breaks = c(-10^(4:0), 0, 10^(0:4)), + trans = "pseudo_log" + ) + +# The log ticks are mirrored when 0 is included +p2 + guides(x = "axis_logticks") + +# To control the tick density around 0, one can set `negative_small` +p2 + guides(x = guide_axis_logticks(negative_small = 1)) +} diff --git a/tests/testthat/_snaps/guides.md b/tests/testthat/_snaps/guides.md index 202b064f29..6e49237a76 100644 --- a/tests/testthat/_snaps/guides.md +++ b/tests/testthat/_snaps/guides.md @@ -52,6 +52,10 @@ Breaks are not formatted correctly for a bin legend. i Use `(, ]` format to indicate bins. +# guide_axis_logticks calculates appropriate ticks + + The `prescale_base` argument will override the scale's log-10 transformation in log-tick positioning. + # binning scales understand the different combinations of limits, breaks, labels, and show.limits `show.limits` is ignored when `labels` are given as a character vector. diff --git a/tests/testthat/_snaps/guides/logtick-axes-with-customisation.svg b/tests/testthat/_snaps/guides/logtick-axes-with-customisation.svg new file mode 100644 index 0000000000..8ad941b27d --- /dev/null +++ b/tests/testthat/_snaps/guides/logtick-axes-with-customisation.svg @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +-100 +-10 +-1 +0 +1 +10 +100 + +10 +100 +1000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +10 +100 +1000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +-100 +-10 +-1 +0 +1 +10 +100 +Negative length pseudo-logticks with 0.1 as smallest tick +Pseudo-logticks with 1 as smallest tick +Inverted logticks with swapped tick lengths +Capped and not-expanded inverted logticks +logtick axes with customisation + + diff --git a/tests/testthat/test-guides.R b/tests/testthat/test-guides.R index 9c335af80b..f8e2b92ae2 100644 --- a/tests/testthat/test-guides.R +++ b/tests/testthat/test-guides.R @@ -331,6 +331,70 @@ test_that("guide_colourbar warns about discrete scales", { }) +test_that("guide_axis_logticks calculates appropriate ticks", { + + test_scale <- function(trans = identity_trans(), limits = c(NA, NA)) { + scale <- scale_x_continuous(trans = trans) + scale$train(scale$transform(limits)) + view_scale_primary(scale) + } + + train_guide <- function(guide, scale) { + params <- guide$params + params$position <- "bottom" + guide$train(params, scale, "x") + } + + guide <- guide_axis_logticks(negative_small = 10) + outcome <- c((1:10)*10, (2:10)*100) + + # Test the classic log10 transformation + scale <- test_scale(log10_trans(), c(10, 1000)) + key <- train_guide(guide, scale)$logkey + + expect_equal(sort(key$x), log10(outcome)) + expect_equal(key$.type, rep(c(1,2,3), c(3, 2, 14))) + + # Test compound transformation + scale <- test_scale(compose_trans(log10_trans(), reverse_trans()), c(10, 1000)) + key <- train_guide(guide, scale)$logkey + + expect_equal(sort(key$x), -log10(rev(outcome))) + + # Test transformation with negatives + scale <- test_scale(pseudo_log_trans(), c(-1000, 1000)) + key <- train_guide(guide, scale)$logkey + + unlog <- sort(pseudo_log_trans()$inverse(key$x)) + expect_equal(unlog, c(-rev(outcome), 0, outcome)) + expect_equal(key$.type, rep(c(1,2,3), c(7, 4, 28))) + + # Test expanded argument + scale <- test_scale(log10_trans(), c(20, 900)) + scale$continuous_range <- c(1, 3) + + guide <- guide_axis_logticks(expanded = TRUE) + key <- train_guide(guide, scale)$logkey + + expect_equal(sort(key$x), log10(outcome)) + + guide <- guide_axis_logticks(expanded = FALSE) + key <- train_guide(guide, scale)$logkey + + expect_equal(sort(key$x), log10(outcome[-c(1, length(outcome))])) + + # Test with prescaled input + guide <- guide_axis_logticks(prescale_base = 2) + scale <- test_scale(limits = log2(c(10, 1000))) + + key <- train_guide(guide, scale)$logkey + expect_equal(sort(key$x), log2(outcome)) + + # Should warn when scale also has transformation + scale <- test_scale(log10_trans(), limits = c(10, 1000)) + expect_snapshot_warning(train_guide(guide, scale)$logkey) +}) + test_that("guide_legend uses key.spacing correctly", { p <- ggplot(mtcars, aes(disp, mpg, colour = factor(carb))) + geom_point() + @@ -546,6 +610,42 @@ test_that("axis guides can be capped", { expect_doppelganger("axis guides with capped ends", p) }) +test_that("logticks look as they should", { + + p <- ggplot(data.frame(x = c(-100, 100), y = c(10, 1000)), aes(x, y)) + + geom_point() + + scale_y_continuous(trans = compose_trans(log10_trans(), reverse_trans()), + expand = expansion(add = 0.5)) + + scale_x_continuous( + breaks = c(-100, -10, -1, 0, 1, 10, 100) + ) + + coord_trans(x = pseudo_log_trans()) + + theme_test() + + theme(axis.line = element_line(colour = "black"), + panel.border = element_blank(), + axis.ticks.length.x.top = unit(-2.75, "pt")) + + guides( + x = guide_axis_logticks( + title = "Pseudo-logticks with 1 as smallest tick", + negative_small = 1 + ), + y = guide_axis_logticks( + title = "Inverted logticks with swapped tick lengths", + long = 0.75, short = 2.25 + ), + x.sec = guide_axis_logticks( + negative_small = 0.1, + title = "Negative length pseudo-logticks with 0.1 as smallest tick" + ), + y.sec = guide_axis_logticks( + expanded = FALSE, cap = "both", + title = "Capped and not-expanded inverted logticks" + ) + ) + expect_doppelganger("logtick axes with customisation", p) + +}) + test_that("guides are positioned correctly", { df <- data_frame(x = 1, y = 1, z = factor("a"))