Search code examples
rggplot2shinytidyeval

Why does geom_histogram not use the mapping given to the ggplot function?


I am currently creating a Shiny app which should build plots according to user input. The user can choose between histograms, density and scatter plots, which variables to depict and what titles/labels to display. Since this is Shiny, I tried to follow the instructions given in https://mastering-shiny.org/action-tidy.html which explain how to use tidy evaluation in this context.

I create a basic plot object with ggplot(..., aes(rlang::.data[[]])) and add the geoms later on. Unfortunately, I then encounter the following error message on trying to display the plot:

Error: stat_bin() requires an x or y aesthetic.

The error replicates without the Shiny environment, so I seem to have gotten something fundamentally wrong. This error not only replicates no matter what geom I use, but also no matter what input variables I use. I am quite confused at this point because the rlang::.data[[]] strategy has worked for me in different contexts before and I thought I had copied the example 12.2.2 in the book. Looking at the resulting gplot, the required aesthetics are present at the highest level, but they aren't used for the layers. So I somehow broke the data flow?

Thus, my question is a rather basic one:

  • Why does the geom not access the mapping information given to the ggplot function?
  • How did I disrupt the data flow from the highest level to the geoms?

I have searched online for an answer and read the documentation, but I don't get it. Also, all of the questions I have found so far deal with the plot not printing or errors because of not considering the data-masking problem, but that somehow seems to be the root of my problem.

Adding the geoms at the same time as I create the plot, does not work either. Nor does adding the mapping information to the geoms themselves. The gplot object still does not contain it in the layers. I haven't tried replicating the problem with base R plots yet because I would prefer using ggplot in the app and I have a feeling that I couldn't replicate it because I introduced the problem by somehow getting the tidy evaluation wrong.

I provided a code example without the Shiny context below.

Thank you very much!

A. S.

library(ggplot2)
library(shiny)
input <- list(
  fill = "gear",
  x = "wt",
  y = "mpg",
  geom = "Histogram"
)

# aesthetics: x, y, fill
if (isTruthy(input$fill)) {
  if (isTruthy(input$y) & input$geom == "Scatter plot") {
    gplot <- ggplot2::ggplot(
      mtcars,
      ggplot2::aes(x = rlang::.data[[input$x]], y = rlang::.data[[input$y]],
                   fill = rlang::.data[[input$fill]])
    )
  } else if (input$geom %in% c("Histogram", "Density plot")) {
    gplot <- ggplot2::ggplot(
      mtcars,
      ggplot2::aes(x = rlang::.data[[input$x]],
                   fill = rlang::.data[[input$fill]])
    )
  } else {
    showNotification("Variable on y-axis of scatter plot still missing.",
                     type = "message")
  }
} else {
  if (isTruthy(input$y) & input$geom == "Scatter plot") {
    gplot <- ggplot2::ggplot(
      mtcars,
      ggplot2::aes(x = rlang::.data[[input$x]], y = rlang::.data[[input$y]])
    )
  } else if (input$geom %in% c("Histogram", "Density plot")) {
    gplot <- ggplot2::ggplot(
      mtcars,
      ggplot2::aes(x = rlang::.data[[input$x]])
    )
  } else {
    showNotification("Variable on y-axis of scatter plot still missing.",
                     type = "message")
  }
}

# geom
plot_geom <- switch(input$geom,
                    "Histogram" = ggplot2::geom_histogram(),
                    "Density plot" = ggplot2::geom_density(),
                    "Scatter plot" = ggplot2::geom_point()
)

# build plot
gplot <- gplot +
  plot_geom

gplot

Solution

  • You appear to be a victim of tidyverse's non-standard evaluation (NSE).

    Roughly speaking, tidyverse functions expect tibble columns as arguments. You're passing characters from the input list.

    Using, for example,

    gplot <- ggplot(mtcars, aes_string(x = input$x, y = input$y, fill = input$fill))
    
    

    produces a plot of the expected form.

    There's probably a more general solution involving the use of enquo and !!, but I don't have time to investigate right now.

    EDIT

    I was close. You need to use get() rather than enquo() and !!. Here's a proof of concept Shiny app.

    library(shiny)
    library(tidyverse)
    
    ui <- fluidPage(
      selectInput("x", "X variable:", choices=c("mpg", "cyl", "disp", "hp", "drat", "wt", "qsec", "vs", "am", "gear", "carb")),
      selectInput("y", "Y variable:", choices=c("mpg", "cyl", "disp", "hp", "drat", "wt", "qsec", "vs", "am", "gear", "carb")),
      selectInput("type", "Plot type:", choices=c("Histogram", "Scatter", "Density")),
      plotOutput("plot")
    )
    
    server <- function(input, output) {
      output$plot <- renderPlot({
        req(input$x, input$y)
        
        if (input$type == "Histogram") {
          mtcars %>% ggplot() + geom_histogram(aes(x=get(input$x)))
        } else if (input$type == "Scatter") {
          mtcars %>% ggplot() + geom_point(aes(x=get(input$x), y=get(input$y)))
        } else if (input$type == "Density"){
          mtcars %>% ggplot() + geom_density(aes(x=get(input$x)))
        }
      })
    }
    
    shinyApp(ui = ui, server = server)