Skip to content

support displaying tabular data in cli condition messages #808

@mcaselli

Description

@mcaselli

Background:

see Stack Overflow

I often like to include a view of relevant tabular data in a warning or error message. When I'm rendering quarto reports I often set chunk options to suppress certain levels of output, e.g. #| message: false to suppress messages but print warnings.

In the code below I'm trying to create output that behaves as a single warning, containing a summary of the warning and a snapshot of the offending data in a formatted table. The clitable package provides nice formatting for tables, but I can't seem to get it incorporated into a warning message with cli_warn() without screwing up the formatting.

library(dplyr)
library(cli)
library(clitable)


check_complete <- function(data, cols) {
  missing <- data |>
    select({{cols}}) |>
    filter(if_any(everything(), ~ is.na(.)))
  
  nrow_missing <- nrow(missing)
  
  if(nrow_missing > 0){
    table_text <- cli_table(missing)
    # include the table in the warning message
    cli_warn(c("There are {nrow_missing} rows with missing values in the specified columns:",
               table_text))
  }
}

check_complete(starwars, c(name:hair_color))
#> Warning: There are 33 rows with missing values in the specified columns:
#> ┌─────────────────────┬──────┬────┬────────────┐
#> │ name │height│mass│ hair_color │
#> ├─────────────────────┼──────┼────┼────────────┤
#> │ C-3PO │ 167 │ 75│ NA │
#> │ R2-D2 │ 96 │ 32│ NA │
#> │ R5-D4 │ 97 │ 32│ NA │
#> │ Wilhuff Tarkin │ 180 │ NA │auburn, grey│
#> ...snip...
#> │ Finn │ NA │ NA │ black │
#> │ Rey │ NA │ NA │ brown │
#> │ Poe Dameron │ NA │ NA │ brown │
#> │ BB8 │ NA │ NA │ none │
#> │ Captain Phasma │ NA │ NA │ none │
#> └─────────────────────┴──────┴────┴────────────┘

The whitespace is all screwed up here.

check_complete2 <- function(data, cols) {
  missing <- data |>
    select({{cols}}) |>
    filter(if_any(everything(), ~ is.na(.)))
  
  nrow_missing <- nrow(missing)

  if(nrow_missing > 0){
    table_text <- cli_table(missing)
    # use cli_verbatim() to preserve the whitespace
    # wrap in cli() to try to treat it as one message
    cli({
      cli_warn("There are {nrow_missing} rows with missing values in the specified columns:")
      cli_verbatim(table_text)
    })
  }
}

# correct formatting
check_complete2(starwars, c(name:hair_color))
#> Warning: There are 33 rows with missing values in the specified columns:
#> ┌─────────────────────┬──────┬────┬────────────┐
#> │ name                │height│mass│ hair_color │
#> ├─────────────────────┼──────┼────┼────────────┤
#> │ C-3PO               │ 167  │  75│ NA         │
#> │ R2-D2               │  96  │  32│ NA         │
#> │ R5-D4               │  97  │  32│ NA         │
#> │ Wilhuff Tarkin      │ 180  │ NA │auburn, grey│
#> │ Greedo              │ 173  │  74│ NA         │
#> ...snip...
#> │ Poe Dameron         │ NA   │ NA │ brown      │
#> │ BB8                 │ NA   │ NA │ none       │
#> │ Captain Phasma      │ NA   │ NA │ none       │
#> └─────────────────────┴──────┴────┴────────────┘

Here we've got the formatting correct, but it's not being classed as a warning, so it'll get suppressed if we suppress messages:

suppressMessages(check_complete2(starwars, c(name:hair_color)))
#> Warning: There are 33 rows with missing values in the specified columns:

How can I keep the formatting of the clitable() while outputting the table as part of a single warning message (or error message, in which case the single message part is even more important since execution will stop on the first error)?

Feature Request:

That cli status messages (inform, warn, abort etc) provide some way of rendering tabular data in the UI, so that developers can display more complex data structures to users. clitable provides one possible rendering approach, but it doesn't play nice with current cli functions.

Some suggested answers for reference

cli_warn uses cli_bullets to format the parameter message which likely messes up your cli_table. A hacky approach could be to replace spaces " " with non-break spaces. Outputing text inside a warning is limited by getOption(warning.length)

warning.length:
sets the truncation limit in bytes for error and warning messages. A non-negative integer, with allowed values 100...8170, default 1000.

I guess you can tackle that limitation by splitting the table and throwing multiple warnings.
But this replacing-approach works and the warning it produces can be suppressed.

library(dplyr)
library(cli)
library(clitable)

check_complete <- function(data, cols) {
  missing <- data |>
    dplyr::select({{cols}}) |>
    dplyr::filter(if_any(dplyr::everything(), ~ is.na(.)))
  
  nrow_missing <- nrow(missing)
  
  if(nrow_missing > 0){
    table_text <- cli_table(missing)
    old <- options(warning.length = 8170)
    on.exit(options(old), add = TRUE)
    # include the table in the warning message
    cli_warn(c("There are {nrow_missing} rows with missing values in the specified columns:",
               gsub(" ", "\u00a0", table_text)))
  }
}
data(starwars, package = "dplyr")

suppressMessages(check_complete(dplyr::bind_rows(replicate(10, starwars, simplify = FALSE)), c(name:hair_color)))

# Warning:
#  There are 330 rows with missing values in the specified columns:
# ┌─────────────────────┬──────┬────┬────────────┐
# │ name                │height│mass│ hair_color │
# ├─────────────────────┼──────┼────┼────────────┤
# │ C-3PO               │ 167  │  75│ NA         │
# │ R2-D2               │  96  │  32│ NA         │
# │ R5-D4               │  97  │  32│ NA         │
# │ Wilhuff Tarkin      │ 180  │ NA │auburn, grey│
# │ Greedo              │ 173  │  74│ NA         │
# │Jabba Desilijic Tiure│ 175  │1358│ NA         │
# │ Mon Mothma          │ 150  │ NA │ auburn     │
# │ Arvel Crynyd        │ NA   │ NA │ brown      │

# ... I cut the output here. The table continues until maximum of 8170 byte chars is hit

res


If you capture this warning log with sink or similar, there might be oddities searching the string for "C-3PO ", if there is actually "\u00a0" instead of " ". But if it's just about a visual table within a warning message, this might work for you. For large tables this will be slow. You can consider cutting off the table before warning

check_complete_cut <- function(data, cols) {
  missing <- data |>
    dplyr::select({{cols}}) |>
    dplyr::filter(if_any(dplyr::everything(), ~ is.na(.)))
  
  nrow_missing <- nrow(missing)
  
  if(nrow_missing > 0){
    table_text <- cli_table(missing[1:min(100, nrow(missing)),])
    old <- options(warning.length = 8170)
    on.exit(options(old), add = TRUE)
    # include the table in the warning message
    cli_warn(c("There are {nrow_missing} rows with missing values in the specified columns (showing max. the first 100 rows with missing values):",
               gsub(" ", "\u00a0", table_text)))
  }
}
data(starwars, package = "dplyr")

check_complete_cut(dplyr::bind_rows(replicate(10, starwars, simplify = FALSE)), c(name:hair_color))

2 Build warning directly with rlang::warn()

Since cli_warn is passed to rlang::warn you might just want to use the latter directly.
Here I recreate the white table text using ANSI colors crayon-style.

check_complete_warn <- function(data, cols) {
  missing <- data |>
    dplyr::select({{cols}}) |>
    dplyr::filter(if_any(dplyr::everything(), ~ is.na(.)))
  
  nrow_missing <- nrow(missing)
  
  if(nrow_missing > 0){
    table_text <- cli_table(missing)
    old <- options(warning.length = 8170)
    on.exit(options(old), add = TRUE)
    rlang::warn(
      message = c(
        sprintf("There are %d rows with missing values in the specified columns", nrow_missing),
        paste0("\n", "\033[37m", paste0(table_text, collapse = "\n"), "\033[39m")
      )
    )
  }
}
data(starwars, package = "dplyr")


suppressMessages(check_complete_warn(dplyr::bind_rows(replicate(10, starwars, simplify = FALSE)), c(name:hair_color)))

res

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions