From f9af13e6c6fb3a8d6350e5c5c5e2aa404142c8aa Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Wed, 10 Jun 2026 20:18:58 -0700 Subject: [PATCH 1/4] issue618 --- R/tinylabel.R | 49 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/R/tinylabel.R b/R/tinylabel.R index e6cc647a..1dcc58b6 100644 --- a/R/tinylabel.R +++ b/R/tinylabel.R @@ -133,36 +133,51 @@ labeller_fun = function(label = "percent") { ## actual formatting functions - format_percent = function(x) { - max_decimals = 5L - pct = as.numeric(x) * 100 - upct = unique(pct) - d = Find( + # Find the smallest number of decimal places (from 0:max_decimals) that keeps + # the unique values distinct, so a single consistent format can be applied to + # the whole vector. Falls back to max_decimals. + consistent_decimals = function(x, max_decimals = 5L) { + ux = unique(as.numeric(x)) + Find( function(d) { - length(unique(sprintf(paste0('%.', d, 'f%%'), upct))) == length(upct) + length(unique(sprintf(paste0("%.", d, "f"), ux))) == length(ux) }, 0:max_decimals ) %||% max_decimals - pct = sprintf(paste0('%.', d, 'f%%'), pct) - return(pct) } - format_comma = function(x) { - prettyNum(x, big.mark = ",", scientific = FALSE) + format_percent = function(x) { + pct = as.numeric(x) * 100 + d = consistent_decimals(pct) + sprintf(paste0("%.", d, "f%%"), pct) } - format_dollar = function(x) { - paste0("$", prettyNum(x, big.mark = ",", scientific = FALSE)) + # Currency convention: keep clean integers integer-valued, but show at least + # two decimal places whenever any fractional component is present (e.g. + # "$0.50" rather than "$0.5"). Negative values place the sign in front of the + # currency symbol (e.g. "-$1.50" rather than "$-1.50"). The comma formatter is + # not currency, so it just uses the smallest consistent number of decimals. + format_currency = function(x, symbol) { + xn = as.numeric(x) + d = consistent_decimals(xn) + has_decimal = any(xn[!is.na(xn)] %% 1 != 0) + if (has_decimal) d = max(d, 2L) + fmt = formatC(abs(xn), format = "f", digits = d, big.mark = ",") + sign = ifelse(!is.na(xn) & xn < 0, "-", "") + paste0(sign, symbol, fmt) } - format_euro = function(x) { - paste0("\u20ac", prettyNum(x, big.mark = ",", scientific = FALSE)) + format_comma = function(x) { + d = consistent_decimals(x) + formatC(as.numeric(x), format = "f", digits = d, big.mark = ",") } - format_sterling = function(x) { - paste0("\u00a3", prettyNum(x, big.mark = ",", scientific = FALSE)) - } + format_dollar = function(x) format_currency(x, "$") + + format_euro = function(x) format_currency(x, "\u20ac") + + format_sterling = function(x) format_currency(x, "\u00a3") format_log = function(x) { x = as.numeric(x) From 7c1d02aa86089cf57305a910a935234490dd36a3 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Wed, 10 Jun 2026 20:19:08 -0700 Subject: [PATCH 2/4] logic tests --- inst/tinytest/test-tinylabel.R | 38 ++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/inst/tinytest/test-tinylabel.R b/inst/tinytest/test-tinylabel.R index dc394988..f014b690 100644 --- a/inst/tinytest/test-tinylabel.R +++ b/inst/tinytest/test-tinylabel.R @@ -10,3 +10,41 @@ f = function() tinyplot( theme = "clean" ) expect_snapshot_plot(f, label = "tinylabel") + +# Currency/comma formatters should use a consistent number of decimal places +# across the whole vector (#618). Currency formatters additionally follow the +# convention of showing at least two decimal places when any fractional +# component is present, while still keeping clean integers integer-valued. +revenue = seq(0, 2.5, length.out = 6) +expect_equal( + tinylabel(revenue, "$"), + c("$0.00", "$0.50", "$1.00", "$1.50", "$2.00", "$2.50") +) +# comma is not currency, so it uses the minimal consistent decimals +expect_equal( + tinylabel(revenue, ","), + c("0.0", "0.5", "1.0", "1.5", "2.0", "2.5") +) +# clean integers stay integer-valued +expect_equal( + tinylabel(c(1000, 2000, 3000), "$"), + c("$1,000", "$2,000", "$3,000") +) +expect_equal( + tinylabel(c(1000, 2000, 3000), ","), + c("1,000", "2,000", "3,000") +) +# NA values are left as-is by default (na.ignore = TRUE) +expect_equal( + tinylabel(c(0, 0.5, NA, 1.5), "$"), + c("$0.00", "$0.50", NA, "$1.50") +) +# negative currency values place the sign in front of the symbol +expect_equal( + tinylabel(c(-1.5, 0, 2), "$"), + c("-$1.50", "$0.00", "$2.00") +) +expect_equal( + tinylabel(c(-1000, 2000), "$"), + c("-$1,000", "$2,000") +) From 6366d85442520384fc92b6d9af8da3016e579fb9 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Wed, 10 Jun 2026 20:19:45 -0700 Subject: [PATCH 3/4] news --- NEWS.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/NEWS.md b/NEWS.md index 71358e47..cef11914 100644 --- a/NEWS.md +++ b/NEWS.md @@ -172,6 +172,15 @@ Theme fixes: - Polygon density hatching lines now correctly use the group colour instead of black. Affects `type_polygon`, `type_chull`, and `type_ellipse` when `density` is set. (#610 @grantmcdermott) +- Fixed inconsistent decimal places in the `tinylabel()` currency and comma + formatters (e.g., `"$"`, `"€"`, `"£"`, `","`). These now use a consistent + number of decimal places across the whole vector, matching the existing + behaviour of the percent formatter. The currency formatters additionally show + at least two decimal places whenever a fractional component is present (e.g. + `"$0.50"` rather than `"$0.5"`), while still keeping clean integers + integer-valued (e.g. `"$1,000"`), and place the negative sign in front of the + currency symbol (e.g. `"-$1.50"` rather than `"$-1.50"`). + (#618 @grantmcdermott) ## v0.6.1 From cdfa6bcc0dd13fefeea274954ba990a54afeba54 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Wed, 10 Jun 2026 20:28:51 -0700 Subject: [PATCH 4/4] unrelated doc tweak --- R/type_barplot.R | 42 ++++++++++++++++++++++++-------------- man/type_barplot.Rd | 49 +++++++++++++++++++++++++++++---------------- 2 files changed, 59 insertions(+), 32 deletions(-) diff --git a/R/type_barplot.R b/R/type_barplot.R index ab28f237..91322502 100644 --- a/R/type_barplot.R +++ b/R/type_barplot.R @@ -57,28 +57,38 @@ #' # wouldn't work for `width`, since it would conflict with the top-level #' # `tinyplot(..., width = )` argument. It's safer to pass these args #' # through the `type_barplot()` functional equivalent. -#' tinyplot(~ cyl | vs, data = mtcars, fill = 0.2, -#' type = type_barplot(beside = TRUE, drop.zeros = TRUE, width = 0.65)) -#' -#' tinytheme("clean2") +#' tinyplot( +#' ~ cyl | vs, data = mtcars, fill = 0.2, +#' type = type_barplot(beside = TRUE, drop.zeros = TRUE, width = 0.65) +#' ) #' #' # Example for numeric y aggregated by x (default: FUN = mean) + facets -#' tinyplot(extra ~ ID | group, facet = "by", data = sleep, -#' type = "barplot", fill = 0.6) +#' tinyplot( +#' extra ~ ID | group, facet = "by", data = sleep, +#' type = "barplot", fill = 0.6, +#' theme = "clean2" +#' ) #' #' # Fancy frequency table: -#' tinyplot(Freq ~ Sex | Survived, facet = ~ Class, data = as.data.frame(Titanic), -#' type = "barplot", facet.args = list(nrow = 1), flip = TRUE, fill = 0.6) +#' tinyplot( +#' Freq ~ Sex | Survived, data = as.data.frame(Titanic), +#' facet = ~ Class, facet.args = list(nrow = 1), +#' type = "barplot", flip = TRUE, fill = 0.6, +#' theme = "clean2" +#' ) #' #' # Centered barplot for conditional proportions of hair color (black/brown vs. #' # red/blond) given eye color and sex -#' tinytheme("clean2", palette.qualitative = c("black", "sienna", "indianred", "goldenrod")) #' hec = as.data.frame(proportions(HairEyeColor, 2:3)) -#' tinyplot(Freq ~ Eye | Hair, facet = ~ Sex, data = hec, type = "barplot", -#' center = TRUE, flip = TRUE, facet.args = list(ncol = 1), yaxl = "percent") +#' hcols = c("black", "sienna", "indianred", "goldenrod") +#' tinyplot( +#' Freq ~ Eye | Hair, data = hec, +#' facet = ~ Sex, facet.args = list(ncol = 1), +#' type = "barplot", center = TRUE, +#' flip = TRUE, yaxl = "percent", +#' theme = list("clean2", palette.qualitative = hcols) +#' ) #' -#' tinytheme() -#' #' # Use cases for the `offset` argument #' #' # 1. Waterfall plot @@ -86,8 +96,10 @@ #' value = c(100, 40, -80, -10, 50)) #' d$item = factor(d$item, levels = d$item) #' d$offset = c(0, cumsum(d$value[1:3]), 0) -#' tinyplot(value ~ item | I(value < 0), data = d, -#' type = type_barplot(offset = d$offset), legend = FALSE) +#' tinyplot( +#' value ~ item | I(value < 0), data = d, +#' type = type_barplot(offset = d$offset), legend = FALSE +#' ) #' tinyplot_add(type = type_vline(4.5), lty = 2) #' #' # 2. Diverging/Likert layout: a character (or named numeric) offset "sets diff --git a/man/type_barplot.Rd b/man/type_barplot.Rd index 0958b4f9..c6944d6d 100644 --- a/man/type_barplot.Rd +++ b/man/type_barplot.Rd @@ -82,27 +82,37 @@ tinyplot(~ cyl, data = mtcars, type = "barplot", xlevels = 3:1) # wouldn't work for `width`, since it would conflict with the top-level # `tinyplot(..., width = )` argument. It's safer to pass these args # through the `type_barplot()` functional equivalent. -tinyplot(~ cyl | vs, data = mtcars, fill = 0.2, - type = type_barplot(beside = TRUE, drop.zeros = TRUE, width = 0.65)) - -tinytheme("clean2") +tinyplot( + ~ cyl | vs, data = mtcars, fill = 0.2, + type = type_barplot(beside = TRUE, drop.zeros = TRUE, width = 0.65) +) # Example for numeric y aggregated by x (default: FUN = mean) + facets -tinyplot(extra ~ ID | group, facet = "by", data = sleep, - type = "barplot", fill = 0.6) +tinyplot( + extra ~ ID | group, facet = "by", data = sleep, + type = "barplot", fill = 0.6, + theme = "clean2" +) # Fancy frequency table: -tinyplot(Freq ~ Sex | Survived, facet = ~ Class, data = as.data.frame(Titanic), - type = "barplot", facet.args = list(nrow = 1), flip = TRUE, fill = 0.6) +tinyplot( + Freq ~ Sex | Survived, data = as.data.frame(Titanic), + facet = ~ Class, facet.args = list(nrow = 1), + type = "barplot", flip = TRUE, fill = 0.6, + theme = "clean2" +) # Centered barplot for conditional proportions of hair color (black/brown vs. # red/blond) given eye color and sex -tinytheme("clean2", palette.qualitative = c("black", "sienna", "indianred", "goldenrod")) hec = as.data.frame(proportions(HairEyeColor, 2:3)) -tinyplot(Freq ~ Eye | Hair, facet = ~ Sex, data = hec, type = "barplot", - center = TRUE, flip = TRUE, facet.args = list(ncol = 1), yaxl = "percent") - -tinytheme() +hcols = c("black", "sienna", "indianred", "goldenrod") +tinyplot( + Freq ~ Eye | Hair, data = hec, + facet = ~ Sex, facet.args = list(ncol = 1), + type = "barplot", center = TRUE, + flip = TRUE, yaxl = "percent", + theme = list("clean2", palette.qualitative = hcols) +) # Use cases for the `offset` argument @@ -111,8 +121,10 @@ d = data.frame(item = c("Sales", "Services", "Costs", "Returns", "TOTAL"), value = c(100, 40, -80, -10, 50)) d$item = factor(d$item, levels = d$item) d$offset = c(0, cumsum(d$value[1:3]), 0) -tinyplot(value ~ item | I(value < 0), data = d, - type = type_barplot(offset = d$offset), legend = FALSE) +tinyplot( + value ~ item | I(value < 0), data = d, + type = type_barplot(offset = d$offset), legend = FALSE +) tinyplot_add(type = type_vline(4.5), lty = 2) # 2. Diverging/Likert layout: a character (or named numeric) offset "sets @@ -125,8 +137,11 @@ lik = expand.grid( ) lik$response = factor(lik$response, levels = unique(lik$response)) lik$share = c( # proportions summing to 1 within each question - .10, .25, .05, .15, .20, .30, .15, .20, .35, .20, .40, .30, - .25, .15, .35, .20, .10, .10, .05, .15 + .10, .25, .05, .15, + .20, .30, .15, .20, + .35, .20, .40, .30, + .25, .15, .35, .20, + .10, .10, .05, .15 ) # diverging palette: reds (disagree) -> blues (agree), grey for "Unsure" pal = c("#b2182b", "#ef8a62", "#67a9cf", "#2166ac", "grey")