Controlling display of numbers

Tibbles print numbers with three significant digits by default, switching to scientific notation if the available space is too small. Underlines are used to highlight groups of three digits. This display works for many, but not for all use cases.

library(pillar)
library(tibble)

Per-column number formatting

The new num() constructor allows creating vectors that behave like numbers but allow customizing their display.

num(-1:3, notation = "sci")
#> <pillar_num(sci)[5]>
#> [1] -1e0  0    1e0  2e0  3e0
tibble(
  x4 = num(8:12 * 100 + 0.5, digits = 4),
  x1 = num(8:12 * 100 + 0.5, digits = -1),
  usd = num(8:12 * 100 + 0.5, digits = 2, label = "USD"),
  percent = num(8:12 / 100 + 0.0005, label = "%", scale = 100),
  eng = num(10^(-3:1), notation = "eng", fixed_exponent = -Inf),
  si = num(10^(-3:1) * 123, notation = "si")
)
#> # A tibble: 5 × 6
#>          x4       x1     usd percent      eng      si
#>   <num:.4!> <num:.1>     USD       %    <eng>    <si>
#> 1  800.5000    800.5  800.50    8.05     1e-3 123   m
#> 2  900.5000    900.5  900.50    9.05    10e-3   1.23 
#> 3 1000.5000   1000.5 1000.50   10.05   100e-3  12.3  
#> 4 1100.5000   1100.5 1100.50   11.05  1000e-3 123    
#> 5 1200.5000   1200.5 1200.50   12.05 10000e-3   1.23k

Computing on num

Formatting numbers is useful for presentation of results. If defined early on in the analysis, the formatting options survive most operations. It is worth defining output options that suit your data once early on in the process, to benefit from the formatting throughout the analysis. We are working on seamlessly applying this formatting to the final presentation (plots, tables, …).

Arithmetics

num(1) + 2
#> <pillar_num[1]>
#> [1] 3
1 + num(2)
#> <pillar_num[1]>
#> [1] 3
1L + num(2)
#> <pillar_num[1]>
#> [1] 3
num(3.23456, sigfig = 4) - num(2)
#> <pillar_num:4[1]>
#> [1] 1.235
num(4, sigfig = 2) * num(3, digits = 2)
#> <pillar_num:2[1]>
#> [1] 12
num(3, digits = 2) * num(4, sigfig = 2)
#> <pillar_num:.2![1]>
#> [1] 12.00
-num(2)
#> <pillar_num[1]>
#> [1] -2

Mathematics

min(num(1:3, label = "$"))
#> <pillar_num{$}[1]>
#> [1] 1
mean(num(1:3, notation = "eng"))
#> <pillar_num(eng)[1]>
#> [1] 2e0
sin(num(1:3, label = "%", scale = 100))
#> <pillar_num{%}*100[3]>
#> [1] 84.14710 90.92974 14.11200

Recovery

The var() function is one of the examples where the formatting is lost:

x <- num(c(1, 2, 4), notation = "eng")
var(x)
#> [1] 2.333333

One way to recover is to apply num() to the result:

num(var(x), notation = "eng")
#> <pillar_num(eng)[1]>
#> [1] 2.333333e0

For automatic recovery, we can also define our version of var(), or even overwrite the base implementation. Note that this pattern is still experimental and may be subject to change:

var_ <- function(x, ...) {
  out <- var(vctrs::vec_proxy(x), ...)
  vctrs::vec_restore(out, x)
}
var_(x)
#> <pillar_num(eng)[1]>
#> [1] 2.333333e0

This pattern can be applied to all functions that lose the formatting. The make_restore() function defined below is a function factory that consumes a function and returns a derived function:

make_restore <- function(fun) {
  force(fun)
  function(x, ...) {
    out <- fun(vctrs::vec_proxy(x), ...)
    vctrs::vec_restore(out, x)
  }
}

var_ <- make_restore(var)
sd_ <- make_restore(sd)

var_(x)
#> <pillar_num(eng)[1]>
#> [1] 2.333333e0
sd_(x)
#> <pillar_num(eng)[1]>
#> [1] 1.527525e0
library(units)
#> udunits database from /Library/Frameworks/R.framework/Versions/4.1-arm64/Resources/library/units/share/udunits/udunits2.xml
set_units.pillar_num <- function(x, ...) {
  unclassed <- x
  class(unclassed) <- NULL
  set_units(unclassed, ...)
}

m <- set_units(1:3, m)
km <- set_units(1:3, km)

tibble(
  sci_int = set_num_opts(m + km, notation = "sci"),
  digits_int = set_num_opts(km + m, digits = 4),
  sci_ext = set_units(num(1:3 + 0.001, notation = "sci"), km)
)
#> # A tibble: 3 × 3
#>   sci_int digits_int sci_ext
#>       [m]       [km]    [km]
#> 1 1.001e3     1.0010 1.001e0
#> 2 2.002e3     2.0020 2.001e0
#> 3 3.003e3     3.0030 3.001e0
tibble(
  sci_int = set_num_opts(m, notation = "sci") + km,
  digits_int = set_num_opts(km, digits = 4) + m,
  sci_ext = set_units(num(1:3, notation = "sci"), m) + km
)
#> # A tibble: 3 × 3
#>   sci_int digits_int sci_ext
#>       [m]       [km]     [m]
#> 1 1.001e3     1.0010 1.001e3
#> 2 2.002e3     2.0020 2.002e3
#> 3 3.003e3     3.0030 3.003e3

formattable

library(formattable)
#> Error in library(formattable): there is no package called 'formattable'
pillar_shaft.formattable <- function(x, ...) {
  pillar::new_pillar_shaft_simple(format(x), align = "right")
}

pillar_shaft.formattable_currency <- function(x, ...) {
  formattable <- attr(x, "formattable", exact = TRUE)

  pillar_shaft(num(unclass(x), digits = formattable$digits))
}

pillar_shaft.formattable_percent <- function(x, ...) {
  formattable <- attr(x, "formattable", exact = TRUE)

  pillar_shaft(num(unclass(x), digits = formattable$digits, label = "%", scale = 100))
}

pillar_shaft.formattable_scientific <- function(x, ...) {
  pillar_shaft(num(unclass(x), notation = "sci"))
}

type_sum.formattable <- function(x) {
  formattable <- attr(x, "formattable", exact = TRUE)

  if (inherits(x, "formattable_currency")) {
    I(sub("^formattable_", "", class(x)[[1]]))
  } else if (inherits(x, "formattable_percent")) {
    I("%")
  } else {
    abbreviate(sub("^formattable_", "", class(x)[[1]]), 4)
  }
}

num_currency(1:3 * 100 + 0.1)
#> Error in num_currency(1:3 * 100 + 0.1): could not find function "num_currency"
num_percent(1:3 * 0.1 + 0.001)
#> Error in num_percent(1:3 * 0.1 + 0.001): could not find function "num_percent"
num_scientific(1:3 * 0.1 + 0.001)
#> Error in num_scientific(1:3 * 0.1 + 0.001): could not find function "num_scientific"
tibble(
  currency = num_currency(1:3 * 100 + 0.1),
  percent = num_percent(1:3 * 0.1 + 0.001),
  scientific = num_scientific(1:3 * 0.1 + 0.001)
)
#> Error in num_currency(1:3 * 100 + 0.1): could not find function "num_currency"

scales

library(scales)

x <- num(1:10 / 100, label = "%", scale = 100)

scales::squish(x)
#> <pillar_num{%}*100[10]>
#>  [1]  1  2  3  4  5  6  7  8  9 10
x < 0
#>  [1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
x < 0L
#>  [1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
scales::cscale(x, scales::rescale_pal())
#> Error in UseMethod("rescale"): no applicable method for 'rescale' applied to an object of class "c('pillar_num', 'pillar_vctr', 'vctrs_vctr', 'double')"

ggplot2

library(ggplot2)

scale_type.pillar_num <- function(x, ...) {
  "continuous"
}

data.frame(x = x, y = 1:10) %>%
  ggplot(aes(x = x, y = y)) %>%
  + geom_point()

Rule-based decoration

library(dplyr)
#> 
#> Attaching package: 'dplyr'
#> The following object is masked from 'package:pillar':
#> 
#>     dim_desc
#> The following objects are masked from 'package:stats':
#> 
#>     filter, lag
#> The following objects are masked from 'package:base':
#> 
#>     intersect, setdiff, setequal, union
data_units <-
  palmerpenguins::penguins %>%
  mutate(across(ends_with("_mm"), set_units, "mm")) %>%
  mutate(across(ends_with("_g"), set_units, "g"))

data_units %>%
  mutate(bill_area = bill_length_mm * bill_depth_mm, .after = island)
#> # A tibble: 344 × 9
#>    species island    bill_area bill_length_mm bill_depth_mm flipper_length_mm
#>    <fct>   <fct>        [mm^2]           [mm]          [mm]              [mm]
#>  1 Adelie  Torgersen    731.17           39.1          18.7               181
#>  2 Adelie  Torgersen    687.3            39.5          17.4               186
#>  3 Adelie  Torgersen    725.4            40.3          18                 195
#>  4 Adelie  Torgersen     NA              NA            NA                  NA
#>  5 Adelie  Torgersen    708.31           36.7          19.3               193
#>  6 Adelie  Torgersen    809.58           39.3          20.6               190
#>  7 Adelie  Torgersen    692.42           38.9          17.8               181
#>  8 Adelie  Torgersen    768.32           39.2          19.6               195
#>  9 Adelie  Torgersen    617.21           34.1          18.1               193
#> 10 Adelie  Torgersen    848.4            42            20.2               190
#> # … with 334 more rows, and 3 more variables: body_mass_g [g], sex <fct>,
#> #   year <int>
data_decor <-
  data_units %>%
  decorate(year, digits = 0) %>%
  decorate(where(is.numeric), digits = 3)
data_decor %>%
  mutate(bill_area = bill_length_mm * bill_depth_mm, .after = island)
#> # A tibble: 344 × 9
#>    species island    bill_area bill_length_mm bill_depth_mm flipper_length_mm
#>    <fct>   <fct>        [mm^2]           [mm]          [mm]              [mm]
#>  1 Adelie  Torgersen   731.170         39.100        18.700           181.000
#>  2 Adelie  Torgersen   687.300         39.500        17.400           186.000
#>  3 Adelie  Torgersen   725.400         40.300        18.000           195.000
#>  4 Adelie  Torgersen    NA             NA            NA                NA    
#>  5 Adelie  Torgersen   708.310         36.700        19.300           193.000
#>  6 Adelie  Torgersen   809.580         39.300        20.600           190.000
#>  7 Adelie  Torgersen   692.420         38.900        17.800           181.000
#>  8 Adelie  Torgersen   768.320         39.200        19.600           195.000
#>  9 Adelie  Torgersen   617.210         34.100        18.100           193.000
#> 10 Adelie  Torgersen   848.400         42.000        20.200           190.000
#> # … with 334 more rows, and 3 more variables: body_mass_g [g], sex <fct>,
#> #   year <int>

`````