rggplot2shinygeospatial

Error in bi_class() function: the condition has length > 1 while using Shiny


I am building an RShiny dashboard that allows the user to select two variables which then is plotted onto a bivariate map. This is the code so far:

library(ggplot2)
library(ggspatial)
library(cowplot)
library(tidyverse)
library(sf)
library(dplyr)
library(shiny)
library(shinyWidgets)
library(biscale)
library(tools)

data <- structure(list(
  mean_pci = c(45676.758, 24152.725, 48797.093, 43445.612, 25898.389, 28334.789, 40320.323, 24833.757, 22664.184, 34335.008), 
  mean_pop = c(2545.349, 9781.295, 6784.606, 2512.415, 4666.243, 3532.699, 4313.023, 8745.735, 6445.338, 5860.371), 
  count_airbnb = c(36, 67, 73, 81, 56, 54, 4, 96, 87, 53), 
  log_mean_price = c(2.285454, 1.820976, 2.112407, 2.157243, 2.341376, 2.235902, 2.503111, 2.368511, 1.818979, 1.795798)
), class = "data.frame", row.names = c(NA, -10L))

ui <- fluidPage(
  
  titlePanel("Bivariate maping"),
  
  sidebarLayout(
    sidebarPanel(
      selectInput("x_variable", h4("Select x variable:"),
                   choices = c("Mean per capita income" = "mean_pci",
                               "Mean population" = "mean_pop",
                               "Number of airbnbs" = "count_airbnb",
                               "Mean price of airbnbs" = "log_mean_price"),
                   selected = "mean_pci"),
      
      selectInput("y_variable", h4("Select y variable:"),
                  choices = c("Mean per capita income" = "mean_pci",
                              "Mean population" = "mean_pop",
                              "Number of airbnbs" = "count_airbnb",
                              "Mean price of airbnbs" = "log_mean_price"),
                  selected = "mean_pci"),
    ),
    
    mainPanel(
      plotOutput("plot", width = "100%", height = "800px")
    )
  )
)

# Define server logic
server <- function(input, output, session){
  plot_reactive <- reactive({
    
    biClasses <- bi_class(data, x = input$x_variable, y = input$y_variable, style = "quantile", dim = 3)
    
    bi_Map <- ggplot()+
      geom_sf(data = biClasses, mapping = aes(fill = bi_class), color = "black", size = 0.1, show.legend = FALSE) +
      bi_scale_fill(pal = "DkViolet", dim = 3) +
      coord_sf(crs = st_crs(7131), datum = NA) +
      theme(plot.margin = unit(c(0.5,0.5,0.5,0.5), "cm"),
            plot.title = element_text(size = 18, face = "bold", hjust = 0.5)) +
      annotation_scale(location = "bl", line_width = 0.2, height = unit(0.05, "in")) +
      annotation_north_arrow(location = "tl", which_north = "true",
                             height = unit(1.25, "cm"), width = unit(1.25, "cm"),
                             style = north_arrow_fancy_orienteering) +
      theme_minimal()
    
    bi_Legend <- bi_legend(pal = "DkViolet", dim = 3, xlab = input$x_variable, ylab = input$y_variable, size = 7)
    
    plot <- ggdraw() +
      draw_plot(bi_Map, 0, 0, 1, 1) +
      draw_plot(bi_Legend, 0.55, 0.358, 0.28, 0.28)
    
    
    })
    
  output$plot <- renderPlot({
    plot_reactive()
  })
}

# Run app
shinyApp(ui = ui, server = server)

I am having the error: the condition has length > 1, which is happening in this line of code:

biClasses <- bi_class(data, x = input$x_variable, y = input$y_variable, style = "quantile", dim = 3)

Note: ui function works and displays correctly.

It is not happy with the input$x_variable, input$y_variable.

When just plotting the map outside a Shiny app and pass mean_pci and mean_pop in the x and y options, the map displays correctly: Note this code is not reproducible without full dataset with geometry

library(ggplot2)
library(ggspatial)
library(cowplot)
library(tidyverse)
library(sf)
library(dplyr)
library(biscale)

biClasses <- bi_class(data, x = mean_pop, y = mean_pci, style = "quantile", dim = 3)

bi_Map <- ggplot()+
  geom_sf(data = biClasses, mapping = aes(fill = bi_class), color = "black", size = 0.1, show.legend = FALSE) +
  bi_scale_fill(pal = "DkViolet", dim = 3) +
  coord_sf(crs = st_crs(7131), datum = NA) +
  theme(plot.margin = unit(c(0.5,0.5,0.5,0.5), "cm"),
        plot.title = element_text(size = 18, face = "bold", hjust = 0.5)) +
  annotation_scale(location = "bl", line_width = 0.2, height = unit(0.05, "in")) +
  annotation_north_arrow(location = "tl", which_north = "true",
                         height = unit(1.25, "cm"), width = unit(1.25, "cm"),
                         style = north_arrow_fancy_orienteering) +
  theme_minimal()

bi_Legend <- bi_legend(pal = "DkViolet", dim = 3, xlab = "Higher pop", ylab = "Higher pci", size = 7)

plot <- ggdraw() +
  draw_plot(bi_Map, 0, 0, 1, 1) +
  draw_plot(bi_Legend, 0.55, 0.358, 0.28, 0.28)

plot

Traceback details for the error:

> traceback()
13: execCallbacks(timeoutSecs, all, loop$id)
12: run_now(timeoutMs/1000, all = FALSE)
11: service(timeout)
10: serviceApp()
9: ..stacktracefloor..(serviceApp())
8: withCallingHandlers(expr, error = doCaptureStack)
7: domain$wrapSync(expr)
6: promises::with_promise_domain(createStackTracePromiseDomain(), 
       expr)
5: captureStackTraces({
       while (!.globals$stopped) {
           ..stacktracefloor..(serviceApp())
       }
   })
4: ..stacktraceoff..(captureStackTraces({
       while (!.globals$stopped) {
           ..stacktracefloor..(serviceApp())
       }
   }))
3: runApp(x)
2: print.shiny.appobj(x)
1: (function (x, ...) 
   UseMethod("print"))(x)

Solution

  • The function bi_class() is expecting column names or vectors as its arguments, but you are proving it with strings with the names of columns. This is because, despite your inputs containing the names of the columns, the object input$x_variable is just a string with the name, not the actual column of the dataframe.

    Therefore, you need to point your function to the actual columns.

    To solve this, we need to use input$x_variable and input$y_variable to actually point to columns in our dataframe.

    Use the mutate function to create two new columns based on your text inputs: "var_x" and "var_y". These two columns will be always expected by the bi_class function, so we only need to create them before we pass data to bi_class. But, in order to pass strings as column names, we need to convert strings into symbols, i.e., tell mutate that out text inputs are actually column names with !!sym():

    #use your inputs to create new columns
            data2 <- data |>
                #the original columns will be renamed 
                mutate(x_variable = !!sym(input$x_variable), 
                       y_variable = !!sym(input$y_variable))
            
            #then use the newly created variables in your function
            biClasses <- bi_class(data2, x = x_variable, y = y_variable, style = "quantile", dim = 3)
    

    Notice that we need to use the injection operator (!!) and the sym function in order to tell mutate that the strings are actually columns in the dataframe.