Function operator check if the first function argument is empty df

Viewed 58

I have a function operator to handle plot errors:

library(ggplot2)
library(dplyr)

handle_plot_error <- function(f) {
    tryCatch(
        f,
        error = function(e) plot_error(e)
    )
}

plot_error <- function(error) {
    ggplot(
        tibble(text = str_wrap(error, 80)),
        aes(x = 1, y = 1, label = text)
    ) +
        geom_text(color = "red") +
        theme_void()
}

I also have a function to create plot facets:

plot_facet <- function(df) { 
    df %>% 
        ggplot(aes(cyl, mpg)) + 
        geom_line() + 
        facet_grid(~am) 
} 

It can happen that plot_facet receives an empty dataframe for input:

handle_plot_error(plot_facet(tibble()))

This error doesn't get handled by my function so I would like improve handle_plot_error to include something like:

if(nrow(df) == 0) { 
    stop("No data available")
}

and then pass this error message to plot_error function. I know I could include stop case into the plot_facet but I prefer it to be in handle_plot_error since I use it for a lot of plotting functions.

Maybe this can be helpful: https://adv-r.hadley.nz/function-operators.html

3 Answers

Your code doesn't catch the error because it occurs only when the plot is build for printing. If you don't want to call print explicitly, as in handle_plot_error(print(plot_facet(tibble()))), you need to build the plot within your tryCatch.

handle_plot_error <- function(f) {
  tryCatch({
    ggplot_build(f)
    f
    },
    error = function(e) plot_error(e)
  )
}

That's a bit inefficient for large and complex plots.

Edit:

You can easily access and test the data of a ggplot object.

handle_plot_error <- function(f) {
  tryCatch({
    stopifnot("Data.frame is empty!" = nrow(f$data) > 0)
    ggplot_build(f)
    f
    },
    error = function(e) plot_error(e)
  )
}

You could define a check nrow(df) == 0 function and then include it in the tryCatch.

Your original handle_plot_error function is not a function operator yet, since function operators take a function as input and return a function as output. This is why I changed the handle_plot_error by wrapping function(…) {} around tryCatch to make it a similar function operator like purrr::possibly. Then you either wrap the function that you want to be handled and then call the arguments like in handle_plot_error(plot_facet)(mtcars) or you define a handled version of your function try_plot_facet <- handle_plot_error(plot_facet) and can then call it in a regular way try_plot_facet(mtcars).

library(ggplot2)
library(dplyr)

check_df <- function(df) {
  if(nrow(df) == 0) { 
    rlang::abort("No data available") # `abort` has nicer error messages
  }
}

handle_plot_error <- function(f) {
  function(...){ 
  tryCatch({
    check_df(...)
    ggplot_build(f(...))
  },
  error = function(e) plot_error(e)
  )
  }
}

plot_error <- function(error) {
  plot <- ggplot(
    tibble(text = stringr::str_wrap(error, 80)),
    aes(x = 1, y = 1, label = text)
  ) +
    geom_text(color = "red") +
    theme_void()
  
  list(plot = plot)
}

plot_facet <- function(df) {
  df %>% 
    ggplot(aes(cyl, mpg)) + 
    geom_line() + 
    facet_grid(~am) 
} 

handle_plot_error(plot_facet)(tibble())
handle_plot_error(plot_facet)(mtcars)

If you have more than one argument in plot_facet we can Name them explicitly in our error handling function:

handle_plot_error <- function(f) {
  function(df, ...){ 
    tryCatch({
      check_df(df)
      ggplot_build(f(df, ...))
    },
    error = function(e) plot_error(e)
    )
  }
}

plot_facet <- function(df, var) {
  df %>% 
    ggplot(aes({{var}}, mpg)) + 
    geom_line() + 
    facet_grid(~am) 
} 

handle_plot_error(plot_facet)(tibble())
handle_plot_error(plot_facet)(mtcars, cyl)

As @Roland says, the error happens when building. His solution is probably best because it will catch other kinds of errors besides just zero rows. But if you really want to give a special error message for those, you can look at the result of the plot expression and see if the included data component has zero rows. For example,

handle_plot_error <- function(f) {
  tryCatch({
    if (!inherits(f, "ggplot"))
       stop("You need to construct a plot.",
            call. = FALSE)
    if (nrow(f$data) == 0)
       stop("Dataset must have at least one row",
            call. = FALSE)
    f
    },
    error = function(e) plot_error(e)
  )
}

You could do both approaches by calling ggplot_build after the checks.

Related