My First Shiny App: A Weekly Meal Planner | Macarena Quiroga

My First Shiny App: A Weekly Meal Planner

Few things are more boring than thinking about what to eat every week. To spare myself the tedium, I created an app to help me.

Image created by me using the {aRtsy} package

I had been meaning to learn Shiny for at least two years, but I never found an excuse to create an app for something I really needed. And a few weeks ago, when I was struggling with the task of having to think about what to eat every day, it dawned on me: a weekly meal planning app, that had to be the project that would finally teach me how to make a shinyApp. Let’s get to work!

What is Shiny App and how does it work?

Shiny is an R package that allows you to create web applications that can be hosted as standalone websites or embedded in other documents like RMarkdown.

From the “New File” option in RStudio, choose the “Shiny Web App” option, which will open a script called “app.R”. There you will find a template app that will help you understand how this logic works. Before we continue, I recommend clicking on “Run App” and playing around with it.

The code is commented (in English) and quite clear, but let’s review its parts. In broad terms, every shinyApp has three parts:

  • The ui, which is the user interface, that is, everything that defines what the user will manipulate or interact with (for example, the input they can give).

  • The server, which is what is actually being executed. It’s the part most similar to a normal R script.

  • The shinyApp() function that connects the previous parts.

In the case of this example, this shinyApp plots a histogram of the waiting time (in minutes) until the next eruption of Old Faithful, a geyser located in Yellowstone National Park in Wyoming (yes, the one from Yogi Bear). The user (us) can control the width of the histogram columns to see the frequencies expressed in different numbers of minutes.

Let’s look at the ui part:

ui <- fluidPage(

    # App title
    titlePanel("Old Faithful Geyser Data"),

    # Sidebar layout with slider control
    sidebarLayout(
        sidebarPanel(
            sliderInput("bins",
                        "Number of bins:",
                        min = 1,
                        max = 50,
                        value = 30)
        ),

        # Define the plot to be displayed as output
        mainPanel(
           plotOutput("distPlot")
        )
    )
)

The entire ui section is contained within ui <- fluidPage(), which is the function that specifies a fluid page layout that will adapt to different screen types. Inside that function, you will find the different elements that the user will see in the shinyApp.

The first of those elements is the title with the titlePanel function, whose mandatory argument is the app’s title, but it also accepts the window title (windowTitle = "...").

The second object defines that the app will have a layout with a sidebar (sidebarLayout()) and a main panel. With the sidebarPanel() function, we define everything that will appear one below the other in the left sidebar, which in this case is only a slider input (sliderInput()) that the user will modify to set the number of bins (bins) the resulting histogram will have. The second element within the layout is the mainPanel(): everything that appears within this function will be the content of the main panel on the right side of the sidebar. In this case, the result (output) of the plot will be displayed, which is why the function is plotOutput(), taking the distPlot object as an argument. Why that argument? We’ll see in a moment.

In summary, this section shows the title (titlePanel()), a sidebar section (siderbarLayout()) where there will be an interactive element (sliderInput()) that allows the user to choose the number of bars the resulting histogram will have, and a main panel (mainPanel()) that defines the type of result that will be in the center of the screen (in this case, the distPlot object).

Now let’s see what we have in the server section:

# Define server logic to create the histogram
server <- function(input, output) {

    output$distPlot <- renderPlot({
        # Generate the bars based on input$bins from the ui section
        x    <- faithful[, 2]
        bins <- seq(min(x), max(x), length.out = input$bins + 1)

        # Create the histogram with the specified number of bars
        hist(x, breaks = bins, col = 'darkgray', border = 'white',
             xlab = 'Waiting time to next eruption (in mins)',
             main = 'Histogram of waiting times')
    })
}

The server section manages the operation that is performed to display the desired result, which is why it has the syntax of a function. It takes two arguments, input, which is the value defined by the user, and output, which is the result object (these two elements are fixed). Inside the function’s code, the output$distPlot object defined in the ui section is built; it then executes the renderPlot() function, which contains the code to create the histogram. This code is nothing special, but take a look at the argument length.out = input$bins + 1: there, the input object defined by the user is called again.

Finally, the ShinyApp ends with a line of code that brings both parts together:

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

While the general structure may seem simple, it has a high degree of complexity. Fortunately, the Posit website has a series of tutorials to learn more about how this tool works.

Now, My ShinyApp

As I mentioned earlier, my first ShinyApp had the challenge of helping me decide what to eat. I am a serial recipe collector, but I have them all disorganized. Creating this app forced me to organize them into a table, which mainly has four columns: the dish name, the category (main course, side dish, dessert), content type (omnivorous, vegetarian, vegan), and the recipe link. It also has other columns that are in progress.

Without further ado, here’s the code for my app:

# Setup -------------------------------------------------------------------

#library(shiny)
library(tidyverse)
library(tibble)
base_comidas <- read.csv("base_comidas.csv", stringsAsFactors = TRUE)

# Define UI 
ui <- fluidPage(
  
  # Application title
  titlePanel("Meal Planner"),
  
  # Sidebar to choose meal type
  sidebarLayout(
    sidebarPanel(
      selectInput(inputId = "meal_type",
                  label = "Meal Types:",
                  choices = levels(base_comidas$Category),
                  selected = "Choose a meal type"),
      
      checkboxGroupInput(inputId = "content",
                         label = "How do you want your meal to be?",
                         choices = levels(base_comidas$Content)),
      
      checkboxGroupInput(inputId = "days",
                         label = "How many days do you want to plan for?",
                         choices = c("Monday", "Tuesday", "Wednesday",
                                     "Thursday", "Friday", "Saturday",
                                     "Sunday")),
      
      checkboxGroupInput(inputId = "time",
                         label = "Which meals do you want to plan?",
                         choices = c("Lunch", "Dinner")),
      
      radioButtons(inputId = "repetitions",
                   label = "Do you want to repeat meals?",
                   choices = c("Yes" = "TRUE",
                               "No" = "FALSE")),
      
      actionButton("plan_button", "Create Plan")
    ),
    
    # Display chosen meal
    mainPanel(
      dataTableOutput(outputId ="meal")
    )
  )
)

# Define server logic 
server <- function(input, output) {
  
  observeEvent(input$plan_button, {
    # Get meal choice
    selection <- base_comidas %>% 
      filter(Category == as.character(input$meal_type) & 
               Content == as.character(input$content)) %>% 
      select(Name)
    
    # Generate all combinations of days and time
    combinations <- expand.grid(days = input$days, time = input$time)
    
    # Randomly choose meal for each combination
    selected_meals <- combinations %>%
      mutate(meal = sample_n(selection, size = nrow(combinations), replace = as.logical(input$repetitions)))
    
    # Create user data frame
    user_meals <- selected_meals %>%
      pivot_wider(values_from = "meal",
                  names_from = "time") %>% 
        rename(Days = days)
    
    # Return table
    output$meal <- renderDataTable(user_meals, options = list(searching = FALSE,
                                                                  paging = FALSE,
                                                                  info = FALSE))
    
  })
}

# Run the application 
shinyApp(ui = ui, server = server)

You can see the online version of the planner here. There are several things I would like to improve; for example, thinking of multiple meals that have the same main ingredient is essential to reduce the amount of shopping and meal preparation time. I would also like to include a variable for “preparation time” or “preparation complexity” to differentiate the availability of each day. And, finally, yes: I have to work on the aesthetic aspect.

As always, remember that you can suscribe to my blog to not miss any updates, and if you have any questions, don’t hesitate to contact me. And if you like what I do, you can buy me a cup of coffee from Argentina or a ko-fi.

Macarena Quiroga
Macarena Quiroga
Linguist/PhD student

I research language acquisition. I’m looking to deepen my knowledge of statistis and data science with R/Rstudio. If you like what I do, you can buy me a coffee from Argentina, or a kofi from other countries. Suscribe to my blog here.

Related