From 737eb5cc89c30c2da24075aa0fe30d750f605db7 Mon Sep 17 00:00:00 2001 From: Teun van den Brand <49372158+teunbrand@users.noreply.github.com> Date: Mon, 21 Oct 2024 14:44:13 +0200 Subject: [PATCH] Minor break function for log10 ticks (#452) --- NAMESPACE | 1 + R/breaks-log.R | 83 +++++++++++++++++++++++++++++ _pkgdown.yml | 1 + man/minor_breaks_log.Rd | 34 ++++++++++++ tests/testthat/_snaps/breaks-log.md | 16 ++++++ tests/testthat/test-breaks-log.R | 23 ++++++++ 6 files changed, 158 insertions(+) create mode 100644 man/minor_breaks_log.Rd create mode 100644 tests/testthat/_snaps/breaks-log.md diff --git a/NAMESPACE b/NAMESPACE index 7515048c..767761be 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -142,6 +142,7 @@ export(log_trans) export(logit_trans) export(manual_pal) export(math_format) +export(minor_breaks_log) export(minor_breaks_n) export(minor_breaks_width) export(modulus_trans) diff --git a/R/breaks-log.R b/R/breaks-log.R index 79acceb4..cbfe7cfc 100644 --- a/R/breaks-log.R +++ b/R/breaks-log.R @@ -82,6 +82,89 @@ breaks_log <- function(n = 5, base = 10) { #' @rdname breaks_log log_breaks <- breaks_log +#' Minor breaks for log-10 axes +#' +#' This break function is designed to mark every power, multiples of 5 and/or 1 +#' of that power for base 10. +#' +#' @param detail Any of `1`, `5` and `10` to mark multiples of +#' powers, multiples of 5 of powers or just powers respectively. +#' @param smallest Smallest absolute value to mark when the range includes +#' negative numbers. +#' +#' @return A function to generate minor ticks. +#' @export +#' +#' @examples +#' # Standard usage with log10 scale +#' demo_log10(c(1, 1e10), minor_breaks = minor_breaks_log()) +#' # Increasing detail over many powers +#' demo_log10(c(1, 1e10), minor_breaks = minor_breaks_log(detail = 1)) +#' # Adjusting until where to draw minor breaks +#' demo_continuous( +#' c(-1000, 1000), +#' transform = asinh_trans(), +#' minor_breaks = minor_breaks_log(smallest = 1) +#' ) +minor_breaks_log <- function(detail = NULL, smallest = NULL) { + if (!is.null(detail) && (!length(detail) == 1 || !detail %in% c(1, 5, 10))) { + cli::cli_abort("The {.arg detail} argument must be one of 1, 5 or 10.") + } + if (!is.null(smallest) && + (!length(smallest) == 1 || smallest < 1e-100 || !is.finite(smallest))) { + cli::cli_abort( + "The {.arg smallest} argument must be a finite, positive, non-zero number." + ) + } + force(smallest) + function(x, ...) { + + has_negatives <- any(x <= 0) + + if (has_negatives) { + large <- max(abs(x)) + small <- smallest %||% min(c(1, large) * 0.1) + x <- sort(c(small * 10, large)) + } + + start <- floor(log10(min(x))) - 1L + end <- ceiling(log10(max(x))) + 1L + + if (is.null(detail)) { + i <- findInterval(abs(end - start), c(8, 15), left.open = TRUE) + 1L + detail <- c(1, 5, 10)[i] + } + + ladder <- 10^seq(start, end, by = 1L) + tens <- fives <- ones <- numeric() + if (detail %in% c(10, 5, 1)) { + tens <- ladder + } + if (detail %in% c(5, 1)) { + fives <- 5 * ladder + } + if (detail == 1) { + ones <- as.vector(outer(1:9, ladder)) + ones <- setdiff(ones, c(tens, fives)) + } + + if (has_negatives) { + tens <- tens[tens >= small] + tens <- c(tens, -tens, 0) + fives <- fives[fives >= small] + fives <- c(fives, -fives) + ones <- ones[ones >= small] + ones <- c(ones, -ones) + } + + ticks <- c(tens, fives, ones) + n <- c(length(tens), length(fives), length(ones)) + + attr(ticks, "detail") <- rep(c(10, 5, 1), n) + ticks + } +} + #' @author Thierry Onkelinx, \email{thierry.onkelinx@inbo.be} #' @noRd log_sub_breaks <- function(rng, n = 5, base = 10) { diff --git a/_pkgdown.yml b/_pkgdown.yml index f788498d..bf7ea339 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -27,6 +27,7 @@ reference: contents: - starts_with("breaks_") - minor_breaks_width + - minor_breaks_log - title: "Bounds: ranges & rescaling" desc: > diff --git a/man/minor_breaks_log.Rd b/man/minor_breaks_log.Rd new file mode 100644 index 00000000..1efb1da2 --- /dev/null +++ b/man/minor_breaks_log.Rd @@ -0,0 +1,34 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/breaks-log.R +\name{minor_breaks_log} +\alias{minor_breaks_log} +\title{Minor breaks for log-10 axes} +\usage{ +minor_breaks_log(detail = NULL, smallest = NULL) +} +\arguments{ +\item{detail}{Any of \code{1}, \code{5} and \code{10} to mark multiples of +powers, multiples of 5 of powers or just powers respectively.} + +\item{smallest}{Smallest absolute value to mark when the range includes +negative numbers.} +} +\value{ +A function to generate minor ticks. +} +\description{ +This break function is designed to mark every power, multiples of 5 and/or 1 +of that power for base 10. +} +\examples{ +# Standard usage with log10 scale +demo_log10(c(1, 1e10), minor_breaks = minor_breaks_log()) +# Increasing detail over many powers +demo_log10(c(1, 1e10), minor_breaks = minor_breaks_log(detail = 1)) +# Adjusting until where to draw minor breaks +demo_continuous( + c(-1000, 1000), + transform = asinh_trans(), + minor_breaks = minor_breaks_log(smallest = 1) +) +} diff --git a/tests/testthat/_snaps/breaks-log.md b/tests/testthat/_snaps/breaks-log.md new file mode 100644 index 00000000..aa7153eb --- /dev/null +++ b/tests/testthat/_snaps/breaks-log.md @@ -0,0 +1,16 @@ +# minor_breaks_log rejects invalid arguments + + Code + minor_breaks_log(7) + Condition + Error in `minor_breaks_log()`: + ! The `detail` argument must be one of 1, 5 or 10. + +--- + + Code + minor_breaks_log(smallest = 0) + Condition + Error in `minor_breaks_log()`: + ! The `smallest` argument must be a finite, positive, non-zero number. + diff --git a/tests/testthat/test-breaks-log.R b/tests/testthat/test-breaks-log.R index be32793e..5cbecb8c 100644 --- a/tests/testthat/test-breaks-log.R +++ b/tests/testthat/test-breaks-log.R @@ -90,3 +90,26 @@ test_that("breaks_log with very small ranges fall back to extended_breaks", { extended_breaks(n = 5)(c(0.95, 3)) )) }) + +test_that("minor_breaks_log has correct amount of detail", { + range <- c(1, 10) + + test <- minor_breaks_log(detail = 1)(range) + expect_true(all(1:10 %in% test)) + + test <- minor_breaks_log(detail = 5)(range) + expect_false(all(1:10 %in% test)) + expect_true(all(c(1, 5, 10) %in% test)) + + test <- minor_breaks_log(detail = 10)(range) + expect_true(all(c(1, 10) %in% test)) + expect_false(5 %in% test) + + test <- minor_breaks_log(detail = 1)(c(-10, 10)) + expect_true(all(-10:10 %in% test)) +}) + +test_that("minor_breaks_log rejects invalid arguments", { + expect_snapshot(minor_breaks_log(7), error = TRUE) + expect_snapshot(minor_breaks_log(smallest = 0), error = TRUE) +})