My First Shinyapp: A Weekly Meal Planner
Few things are more boring than thinking about what to eat every week. To avoid the tedium, I made an app to help me.
For at least two years, I had the idea of learning Shiny, but I never found an excuse to build an app for something I truly needed. A few weeks ago, while struggling with the daily task of deciding what to eat, I had an epiphany: a weekly meal planning app, that had to be the project with which I would finally learn to make a shinyApp. Let’s get to work!
What is a Shiny App and How Does It Work?
Shiny is an R package that allows you to create web applications, which can be hosted as standalone websites or embedded in other documents like RMarkdown.
From the new file option in RStudio, we choose the Shiny Web App option, which will open a script called app.R. There, we will find a model app that will help us understand how this logic works. Before continuing, I recommend that you click Run App and play around with it a bit.
The code is commented (in English) and quite clear, but let’s review its parts. Broadly speaking, every shinyApp has three parts:
The ui, which is the user interface, meaning everything that defines what the user will manipulate or interact with (for example, the input they can provide).
The server, which is what is actually being executed. It is 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 frequency (in minutes) until the next eruption of Old Faithful, a geyser located in Yellowstone Park in Wyoming (yes, the one from Yogi Bear). The user (us) can control the width of the histogram bins to see the frequencies expressed in different minute intervals.
Let’s look at the ui part:
ui <- fluidPage(
# Título de la app
titlePanel("Old Faithful Geyser Data"),
# Barra lateral con control deslizante
sidebarLayout(
sidebarPanel(
sliderInput("bins",
"Number of bins:",
min = 1,
max = 50,
value = 30)
),
# Define el gráfico que se muestra como resultado
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. Within this function, the various elements that the user will see in the shinyApp will appear.
The first of these elements is the title with the titlePanel function, whose mandatory argument is the app’s title, but which 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 control (sliderInput()) that the user will modify to define the number of bins (bins) the 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, to the right of the sidebar. In this case, the 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 us the title (titlePanel()), a sidebar section (sidebarLayout()) where there will be an interactive element (sliderInput()) that will allow the user to choose the number of bins 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).
Let’s see what we have in the server section:
# Define la lógica del servidor para crear el histograma
server <- function(input, output) {
output$distPlot <- renderPlot({
# genera las barras a partir de input$bins de la sección ui
x <- faithful[, 2]
bins <- seq(min(x), max(x), length.out = input$bins + 1)
# crea el histograma con la cantidad especificada de barras
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 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). And within the function’s code, the output$distPlot object, which was defined in the ui section, is constructed; in turn, this executes the renderPlot() function, which contains the code to create the histogram. This code is not unusual, but notice the length.out = input$bins + 1 argument: there, the input object, which is defined by the user, is called again.
Finally, the ShinyApp ends with a line of code that unifies both parts:
# Corre la app
shinyApp(ui = ui, server = server)
While the general structure seems 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 above, my first ShinyApp had the challenge of helping me decide what to eat. I am a serial recipe hoarder, but I have them all disorganized. Building this app forced me to organize them into a table, which mainly has four columns: the name of the dish, the category (main, side, dessert), content type (omnivorous, vegetarian, vegan), and the link to the recipe. It also has other columns that are currently in preparation.
Without further ado, the code for my app is as follows:
# Setup -------------------------------------------------------------------
#library(shiny)
library(tidyverse)
library(tibble)
base_comidas <- read.csv("base_comidas.csv", stringsAsFactors = TRUE)
# Define UI
ui <- fluidPage(
# Application title
titlePanel("Planificador de comida"),
# Sidebar para elegir tipo de comida
sidebarLayout(
sidebarPanel(
selectInput(inputId = "tipo_comida",
label = "Tipos de comida:",
choices = levels(base_comidas$Categoría),
selected = "Elija un tipo de comida"),
checkboxGroupInput(inputId = "contenido",
label = "¿Cómo desea que sea su comida?",
choices = levels(base_comidas$Contenido)),
checkboxGroupInput(inputId = "dias",
label = "¿Para cuántos días quiere planificar?",
choices = c("Lunes", "Martes", "Miércoles",
"Jueves", "Viernes", "Sábado",
"Domingo")),
checkboxGroupInput(inputId = "momento",
label = "¿Qué comidas quiere planificar?",
choices = c("Almuerzo", "Cena")),
radioButtons(inputId = "repeticiones",
label = "¿Quiere repetir las comidas?",
choices = c("Sí" = "TRUE",
"No" = "FALSE")),
actionButton("plan_button", "Armá el plan")
),
# Mostrar la comida elegida
mainPanel(
dataTableOutput(outputId ="comida")
)
)
)
# Define server logic
server <- function(input, output) {
observeEvent(input$plan_button, {
# Obtener la elección de comida
eleccion <- base_comidas %>%
filter(Categoría == as.character(input$tipo_comida) &
Contenido == as.character(input$contenido)) %>%
select(Nombre)
# Generar todas las combinaciones de días y momentos
combinaciones <- expand.grid(dias = input$dias, momento = input$momento)
# Elegir comida al azar para cada combinación
comidas_seleccionadas <- combinaciones %>%
mutate(comida = sample_n(eleccion, size = nrow(combinaciones), replace = as.logical(input$repeticiones)))
# Crear data frame de usuario
comidas_user <- comidas_seleccionadas %>%
pivot_wider(values_from = "comida",
names_from = "momento") %>%
rename(Días = dias)
# Devolver tabla
output$comida <- renderDataTable(comidas_user, 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 several meals that share a main ingredient is fundamental to reduce the amount of shopping and meal preparation time. I would also like to include a “preparation time” or “preparation complexity” variable, to differentiate the availability for each day. And, finally, yes: I need to work on the aesthetic aspect.