Search code examples
rshinydplyrtidyeval

How can I create a data.frame based on variable number of tidyeval inputs


First of all apologies for the somewhat uninformative title

I have a shiny app where a user downloads one of many possible datasets and for some columns can perform a filter to produce a data.frame output

I want to standardize the code irrespective of the dataset downloaded

The problem is that the column names differ by dataset and there will be a variable number of columns I wish to filter on

As far as creating inputs are concerned, I have adapted this solution using a tidyeval approach. However, I am having difficulty with the output without having to resort to a lot of if else statements based on number of columns that can be filtered on

Here is an (non-shiny) example based on a dataset where I have 2 filterable columns, a Value column which is always required and one column unwanted in the final output

library(tidyverse)

## desired columns 
my_cols <- c("col 1", "another col")

# selected input
input_1 <- c("A","B")
input_2 <- c("Z")

l <- list(`col 1` = rep(c("A","B","C"),times=3), `another col` = 
rep(c("X","Y","Z"),each=3), Value = c(1:9),`Unwanted 1`=(9:1))

df <- as_tibble(l)

# this creates the right number of correctly-named columns
for (i in seq_along(my_cols)) {
assign(paste0("col_", i), sym(my_cols[i]))
}

## This produces output but wish to adapt
## to varying number of columns

df %>%
filter(!!col_1 %in% input_1) %>%
filter(!!col_2 %in% input_2) %>%
select(!!col_1, !!col_2, Value)

#  `col 1` `another col` Value
#  <chr>   <chr>         <int>
# 1 A       Z                 7
# 2 B       Z                 8

So it is the last piece of code I wish to adapt to take account of the variable length of my_cols

TIA


Solution

  • You seem to store input in separate variables, with suggests you know up front how many columns will be operated on (unless those are coming from dynamically generated UI).Anyways, I suggest you keep inputs in one object as well (hopefully same length as my_cols, otherwise you could subset the input list to match the length of the my_cols vector). Then you can prepare a list of quosures and splice them into filter and select.

    library(tidyverse)
    library(rlang)
    
    ## desired columns 
    my_cols <- c("col 1", "another col")
    
    # selected input
    input_1 <- c("A","B")
    input_2 <- c("Z")
    input_3 <- NULL # ui handle that is not used for this dataset
    
    l <- list(`col 1` = rep(c("A","B","C"),times=3), `another col` = 
                rep(c("X","Y","Z"),each=3), Value = c(1:9),`Unwanted 1`=(9:1))
    
    df <- as_tibble(l)
    
    # make a list of inputs
    l_input <- list(input_1, input_2, input_3)[seq_along(my_cols)]
    
    # make a list of expression quosures. Make sure you use enquos if inside function 
    l_expr <- mapply(function(x,y) quos(!!sym(x) %in% !!y), my_cols, l_input, USE.NAMES = F)
    # splice into filter and select
    df %>% filter(!!!l_expr) %>%  select(!!!syms(my_cols), Value)
    

    If you put this inside a function, remember to use enquos()