All About Shiny Modules

Introduction

Anyone who builds Shiny app will know how long the R scripts can get. The more complicated the shiny app is, the lengthier the R scripts will be. Shiny Modules can save us from long R scripts.

But this is not the only benefit of using Shiny Modules. Modules also help with the namespace problem that occurs in the Shiny UI and server logic by adding a level of abstraction beyond functions. Functions are the basic level of abstraction in R, so we can come up with functions that return UI elements and reusable server logic.

But hey, I want my UI elements to have specific IDs and not hard code them inside the functions. Input and output IDs in Shiny apps share a global namespace and we need to make sure that the IDs are unique within the namespace. Shiny modules address this problem by adding a level of abstraction that is beyond the functions.

Too many words, I know, let’s get down to business and create a simple shiny app that uses shiny modules. In order to do this, we will be using the Tidy Tuesday Spotify Songs data set. You can access the Shiny App here to see what the app looks like.

Setting up the files

Depending on the app we build we may have 2 separate files for server and UI, or just stick to one app.R file. I will be using just an app.R file because the app is pretty simple. Additionally, we will need a file that stores all our modules for the UI and server logic and this is where the magic will happen.

##               levelName
## 1 Spotify-Data-Analysis
## 2  ¦--app.R            
## 3  °--plotModule.R

Data Preparation

Once the files have been created we can start building the app. This app will basically compare the differences in the various attributes related to music for each of the genres. The different genres that we will be comparing are

  • pop
  • rap
  • rock
  • latin
  • r&b
  • edm

across the attributes

  • track_popularity
  • danceability
  • energy
  • loudness
  • speechiness
  • acousticness
  • instrumentalness
  • liveness
  • valence
  • tempo

So let’s start by reading the data and only selecting the variables that are of interest to us.

library(data.table)

# Reading the data and selecting the columns that we are interested in.
spotify_songs <- data.table::fread('https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2020/2020-01-21/spotify_songs.csv')

spotify_songs <- spotify_songs[,.(track_popularity, playlist_genre, 
                                  danceability, energy, loudness, 
                                  speechiness, acousticness, instrumentalness, 
                                  liveness, valence, tempo)]

spotify_songs <- melt(spotify_songs, id.vars = c("playlist_genre"))

Building the UI of the app

Namespaces

Modules are coding patterns that are organised into 2 functions. A function that creates the UI elements and a function that loads the server logic. Both these functions will share a namespace, thereby making things easier for us to access. To orchestrate all this, we require a simple function called NS() which creates namespaced ids out of bare ids. This is necessary because the ids in a Shiny app needs to be unique for each element. Let’s look at an example:

library(shiny)
# The id of the UI element
id <- "Loudness"

# Create a namespace
ns <- NS(id)

# Generate unique Ids
ns("genre")
## [1] "Loudness-genre"
ns("plot")
## [1] "Loudness-plot"

In our case, we will be creating a function named plotModuleInput() that will hold the UI elements that need to be populated into the shiny app and a plotModule() function that will provide the outputs for the UI. The function plotModuleInput() will need to take in the ID as an argument to create the namespace and then generate unique IDs for each of the UI elements. Let’s do this in the plotModule.R file.

plotModuleInput <- function(id){
  # Create a namespace with the id
  ns <- NS(id)
}

Tag List

A module is not a normal function, and since we are returning a UI element from the plotModuleInput(), we need to put all the UI elements inside a tagList(). This creates an R object that represents a HTML tag. Inside this, we will be adding the 2 UI items that will be present in all the tabs, the selectInput() for selecting the various genres and the plotlyOutput() for showing the bar graph. The ids that we pass in as an argument to them will be built using the namespace that we just created.

plotModuleInput <- function(id){
  # Make a namespace function
  ns <- NS(id)
  
  # This ns function can then be used to wrap each of the id's with the appropriate attributes
  tagList(selectInput(ns("genre"), "Select Genres:",
                      c("Pop" = "pop", 
                        "Edm" = "edm", 
                        "Rap" = "rap", 
                        "Rock" = "rock", 
                        "Latin" = "latin", 
                        "R&B" = "r&b"), multiple = TRUE),
          plotlyOutput(ns("plot"))
  )
}

And guess what… the UI part of the app is completely done and we just need to call it in our app.R file with the appropriate genres plugged into them. Let us add that into the app.R file.

# UI of the Shiny app which has different tab panels for different attributes in music
ui <- fluidPage(
  tabsetPanel(type = "tabs", 
              tabPanel("Overall", plotModuleInput("Overall")),
              tabPanel ("Track Popularity", plotModuleInput("Track Popularity")),
              tabPanel ("Danceability", plotModuleInput("Danceability")),
              tabPanel ("Energy", plotModuleInput("Energy")),
              tabPanel ("Loudness", plotModuleInput("Loudness")),
              tabPanel ("Speechiness", plotModuleInput("Speechiness")),
              tabPanel ("Acousticness", plotModuleInput("Acousticness")),
              tabPanel ("Instrumentalness", plotModuleInput("Instrumentalness")),
              tabPanel ("Liveness", plotModuleInput("Liveness")),
              tabPanel ("Valence", plotModuleInput("Valence")),
              tabPanel ("Tempo", plotModuleInput("Tempo"))
))

Server logic for the app

The server function plotModule() in the plotModule.R file will take in the same arguments as a normal server function would with along with sessions and data arguments. The session argument is an environment that will give us access to information and functionality relating to the session. The data argument will be passed as a parameter from the server function which will hold the data that needs to be plotted. Note that this parameter is optional and will not be useful in other circumstances.

plotModule <- function(input, output, session, data){}

Inside this function, we can provide the output that is needed by accessing the output parameter. Note that we do not need to access each of the UI elements by ID that were created in the plotModuleInput function. This is automatically passed in by the session variable. So, we just have to mention if it was the plot.

plotModule <- function(input, output, session, data){
  output$plot <- renderPlotly({
    if (length(input$genre) != 0){
      data <- data[playlist_genre %in% input$genre, ]
    }
    
    bar_graph <- ggplot(data, aes(x = playlist_genre, y = Mean, fill = playlist_genre, 
                                  text = paste0("Playlist Genre: ", playlist_genre, "\nMean: ", round(Mean,2)))) + 
      geom_col() + scale_fill_brewer(palette = "Paired") +
      theme_light() +
      xlab("Playlist Genre") +
      theme(legend.position = "none")
    
    ggplotly(bar_graph, tooltip = "text")
  })
}

With that, the server logic for the app is also complete. Now, in the app.R file, we just have to call these modules along with their respective datasets. To get the respective data for each of the attributes, we build a function preareData that filters the spotify dataset for that particular attribute and then groups it by the playlist_genre and gives the mean of the value. We use a function called callModule() to call the server logic with the respective datasets.

prepareData <- function(attribute){
  spotify_songs[variable == attribute,.(Mean=mean(value)),.(playlist_genre)]
}
# Server logic for each of the tabs
server <- function(input, output){
  callModule(plotModule, "Overall", spotify_songs[,.(Mean=mean(value)),.(playlist_genre)])
  callModule(plotModule, "Track Popularity", prepareData('track_popularity'))
  callModule(plotModule, "Danceability", prepareData('danceability'))
  callModule(plotModule, "Energy", prepareData('energy'))
  callModule(plotModule, "Loudness", prepareData('loudness'))
  callModule(plotModule, "Speechiness", prepareData('speechiness'))
  callModule(plotModule, "Acousticness", prepareData('acousticness'))
  callModule(plotModule, "Instrumentalness", prepareData('instrumentalness'))
  callModule(plotModule, "Liveness", prepareData('liveness'))
  callModule(plotModule, "Valence", prepareData('valence'))
  callModule(plotModule, "Tempo", prepareData('tempo'))
}

We have successfully created the module and the app. In order to link the app and module, just add source("plotModule.R") at the top of your app.R file.

Conclusion

Building an app can seem fun and daunting at the same time. Fun because web development got a lot easier with it, daunting because of the long chunks of code that we end up with after building the app. Shiny modules helps in modularizing the code into understandable parts and also removing repetition of code. I am glad I got introduced to this, hope you do too!