How to Build an App in R Shiny | by Mate Pocs | Towards Data Science

Shiny is an R package that lets you build interactive web apps. All you need is R, no HTML, CSS, or JavaScript — although you certainly have the option to enhance your app with them. You can run the app on your computer, host on your own server, or use RStudio’s cloud service.
In this post, I am going to walk through the process of building a simple data analysis app from scratch. You can find a lot of excellent guides for the basics out there, my aim was to take it a few tiny steps further: we are going to do some dynamic UI updates, dynamic outputs, an action button, panel navigation, etc.
Please note that it’s not a “serious” data analysis app, my purpose was to showcase different functionalities of shiny that might come in handy. However, once we are finished, it should be pretty easy to add new functionalities to our code.
You can have a look at the app we are going to build on shinyapps.io: https://matepocs.shinyapps.io/data_analyser/.
The underlying code is on my GitHub.
Shiny is a very well documented package, with excellent guides on its website. In fact, it’s a bit too well documented, and it’s easy to get lost in the maze of getting started videos, tutorials, written guides, articles, etc.
I personally found Mastering Shiny by Hadley Wickham to be a very good and compact source, highly recommend it.
This is the high-level design sketch of what we want our app to be able to do.
The user:
imports a .csv file;
selects up to 2 numerical and 1 factor type variables from the columns.
Then the app:
draws a plot;
creates summary tables.
The funny part is that we want the outputs to be different based on what type of columns we selected — we want a different plot if it’s just a factor variable selected, and a more complicated one if we have numerical variables by a factor.
We are going to work in RStudio and R. To use shiny, you first need to install the package:
install.packages(“shiny”)
(The other packages we are also going to use: data.table, ggplot2, you might need to install them too. I assume familiarity with these packages going forward.)
Next, we need to create a folder with the name of the app. I called mine data_analyser. We will keep working inside this folder.
The framework of a shiny app is pretty simple: one R script that has to be called app.R. So we are going to put one inside our data_analyser folder.
(Note: you might find the framework confusing if you read some older guides. Instead of an app.R that contains the UI and the server functionalities, you used to need precisely two scripts: a server.R, and a ui.R. That old approach still works for legacy reasons, but I think it’s not recommended. Additionally, you can have different scripts in the folder, and source them in your app script. So you don’t have to put everything in one script like we are going to do now, you can store your server function in a separate server.R script and source it, just know that it’s not a requirement.)
The minimum Shiny app framework looks something like this:
library(shiny)ui <- fluidPage()server <- function(input, output){}shinyApp(ui = ui, server = server)
You can copy this to your app.R, and should be able to run it by either using a keyboard shortcut (like Cmd + Shift + Return) or by clicking on the Run App button in RStudio (which will automatically recognise that it’s a Shiny app):
(Another way of running the app locally is by simply typing runApp(.../data_analyser) to the console, adding the path to the folder of course.)
If you run the app, you should see an empty window popping up. Which is expected, but time to change that!
As the framework suggests, a Shiny app has two parts: the user interface, and the server.
The ui object is responsible for the layout of the app. It will decide what kind of pages or tabs to show, how to format the text that appears, what kind of inputs to require from the users, and where to put the outputs. (We don’t know what the outputs are exactly yet, but we know where each of them will go!)
The server is a function that defines the connection between the input and output data frames. It’s basically a list of observers, blocks of code looking for changes in input values or other observers (just to keep things funny) to determine whether an output showing on the screen should be updated or not.
We are going to concentrate on the layout first, which is defined in the ui object.
First, let’s get the pages and tabs in. When we started typing in the first prototype, we defined ui as a fluidPage. A lot of beginners’ guides I found used fluidPage, specifically a sidebarLayout, so let’s do something different this time!
We want to create separate pages for our app, like for a proper website, and we can use navbarPage for that purpose. This will create a header on top with links to the different pages. Let’s put in two pages:
ui <- navbarPage(  title = "Data Analyser",   main_page,   about_page)
The code will not run just yet, we need to define the two pages. Both need to be a tabPanel object. They can’t be fluidPages. (And if you start to find this confusing and ad-hoc, you are not alone, but we are almost out of new page-building concepts.)
Let’s put in the two pages:
main_page <- tabPanel(title = “Analysis”)about_page <- tabPanel(title = “About”)
If you run the app now, you should see the navigation panel at the top.
You can put some general information on the About page. You have a number of options when formatting text, have a look here for the full list. This page only exists to showcase how different pages would work.
about_page <- tabPanel( title = “About”, titlePanel(“About”), “Created with R Shiny”, br(), “2021 April”)
(Note: You can use different titles on the page itself and on the navigation panel.)
The Analysis page is going to be more exciting. First, we want the inputs to be in a sidebar on the left. Then, we want a separate plot and table area on the right. In Shiny terms: our main page, the tabPanel is going to be a sidebarLayout, with an “Inputs” sidebarPanel on the left, and a mainPanel on the right, which will take the form of a tabsetPanel, with two tabPanels. Huhh.
I feel like there is no reason for me to summarise what these do, have a look at the links if you are curious, but the names should be self-explanatory.
This is the skeleton of the main page:
main_page <- tabPanel(  title = "Analysis",  titlePanel("Analysis"),  sidebarLayout(    sidebarPanel(      title = "Inputs"    ),    mainPanel(      tabsetPanel(        tabPanel(          title = "Plot"        ),        tabPanel(          title = "Statistics",        )      )    )  ))
And if you run the app now, this is what you should see:
(Note: it’s a good time to check what shiny is doing with these weird building blocks. If you simply type the main_page part of the script in the console, you will see how shiny converts it into HTML.)
Next, we are going to fill up the inputs section with different user input forms. Text or number inputs, sliders, dropdown selections, radio buttons, etc. are collectively called widgets in the Shiny universe. (You can find the comprehensive list here.)
Whenever you add an input widget, the first parameter is going to be its ID, that is how we can refer to that specific input later in the server part. The second parameter is the label — a text field that is placed around the interface, informing the user about its purpose.
Once again, we could list the details of the individual types, but it’s mostly quite straightforward. We will need fileInput, selectInput, and an actionButton, have a read at the official site for more details. We can insert the following code inside the “Inputs” sidebarPanel on main_page:
fileInput("csv_input","Select CSV File to Import",accept=".csv"),selectInput("num_var_1","Numerical Variable 1",choices=c(not_sel)),selectInput("num_var_2","Numerical Variable 2",choices=c(not_sel)),selectInput("fact_var","Factor Variable",choices=c(not_sel)),actionButton("run_button","Run Analysis",icon=icon("play"))
Notes:
Inputs separated by comma will be nicely arranged under each other.
Later, we will refer to these inputs as input$csv_input, input$num_var_1, etc.
In the selectInput field, I thought it would be nice to start with a selection that informs the user that nothing is selected yet. That’s why the not_sel = “Not Selected” global variable was born.
You can add an icon from the Font Awesome and Glyphicons libraries to the page. You don’t have to install anything extra for this functionality.
If you run your app now, this is what you should see:
All we have left to do is decide where the outputs are going to go, and we are done with the UI.
The outputs follow a similar logic to the inputs. At this point, all we determine is:
the position on the screen;
the type (shiny has to know in advance if it’s going to be a text, table, plot, etc);
and the ID.
Just like with the inputs, we can simply add the fields in the corresponding areas of the main_page, and shiny will intelligently fill up the available space.
On the Plot tab, all we want to add is a plotOutput:
plotOutput(“plot_1”)
The Statistic tab is going to be more complicated. We are going to utilise the fluidRow functionalities to place different items next to each other. (Note how so far every comma resulted in the next line being rendered underneath the first one. We want to avoid that with the small tables we are going to create, and put them next to each other.)
This is the Statistic tabPanel after the updates:
tabPanel(  title = "Statistics",  fluidRow(    column(width = 4, strong(textOutput("num_var_1_title"))),    column(width = 4, strong(textOutput("num_var_2_title"))),    column(width = 4, strong(textOutput("fact_var_title")))  ),  fluidRow(    column(width = 4, tableOutput("num_var_1_summary_table")),    column(width = 4, tableOutput("num_var_2_summary_table")),    column(width = 4, tableOutput("fact_var_summary_table"))  ),  fluidRow(    column(strong("Combined Statistics"))  ),  fluidRow(    column(tableOutput("combined_summary_table"))  )
In the first line, we are going to have three titles, for the three selectable variables. This needs to be an output because it dynamically changes as we select different columns.
Next, we put in the three 1-dimensional summary tables. We don’t have to worry about what they are yet, we just know there will be tables there under the titles.
And finally, we put in one combined summary table that will explore relationships between the selected variables.
Notes:
The width parameters of the columns show their relative size, following the Bootstrap grid system. (Shiny uses the same framework.)
Putting the text in strong() makes it bold.
All right, the outputs are placed. If you run the app now, you’ll see that... nothing has changed since last time. It’s time to connect the inputs and the outputs!
Before we dive into the steps of building our app, I think it would be beneficial to quickly summarise how reactive programming manifests in Shiny. For a more detailed guide, have a look at this page.
The server function is basically going to be a long list of expressions connecting inputs to outputs. Interestingly, the order of these expressions does not usually matter, although you probably want to keep some logic.
Most outputs shown on the screen will eventually come from a render function. You use renderText for a textOutput, renderPlot for a plotOutput, etc. The type of render function used will depend on the type of output. For example, if you have defined a textOutput in the ui with an ID of “book”, and want to show the text “Dune” there, this is how you do it:
output$book <- renderText(“Dune”)
Observe how you need to render the text “Dune”, even if it would seem to be pretty texty already!
Of course, you would probably want to keep the output dynamic — otherwise, you can just print the title on the page.
The next step is to connect this to an input. Let’s say you have a textInput field with the ID of “title”. If you add the following line:
output$book <- renderText(input$title)
whenever you update the title field, the book output will update too.
That’s the beauty of the whole system, you don’t have to determine what happens if the user updates the title field — you already established the connection, the book output is watching like a hawk for any changes.
As a next step, you can create so-called reactive expressions: these are a sort of intermediary steps between inputs and outputs. For example, if you want to keep the sum of two number inputs, you can keep them in one variable:
sum_of_numbers <- reactive(input$num_1 + input$num_2)
And then use sum_of_numbers as if it were an input. Note that sum_of_numbers is a function, so when you refer to its value in the code, you must put brackets after it (and this is definitely something one tends to forget):
output$sum <- renderText(sum_of_numbers())
This constant vigilance from the outputs part sounds great at first, but you soon realise the drawbacks: there are situations where you definitely DON’T want to update the output field every time the input changes. This can be achieved by an eventReactive function, which is a reactive expression with an extra input: it only runs if a value of a selected input field changes.
This might sound confusing at first but will become much clearer once we have a look at the practical steps.
Just one more formatting note: in the server function, most of the expressions follow this format:
variable_to_create <- some_reactive_function({     code_to_execute})
(This is also true for the whole server function.) The curly brackets are simply there to allow you to put codes in multiple rows. If you can fit the code in one row, you can omit them.
All right, now that we know the theory, let’s start applying it in a very roundabout way!
Recall that we have an input item called input$csv_input. Once populated, this is actually a data.frame, one of its elements being the datapath. (Have a look here for more info.) Once we know the path to the file, we can read it into a data.table, exactly like how we would normally read in a file, with fread.
However, we need one more ingredient. If we simply try to load in whatever is in the input field, the app will start by trying to load in an empty file and crash immediately. In order to prevent that, we need to add a req condition: Shiny’s way of making sure we actually have the input necessary.
So this is going to be our first block in the server function:
data_input <- reactive({    req(input$csv_input)    fread(input$csv_input$datapath)  })
Now, whenever we upload a .csv file, the app will automatically save its results in thedata.table called data_input. Well, not quite. More precisely, we have a data_input function, that returns a data.table. (This is very easy to mess up!)
(Note: I am not doing any validations on the data type just yet. If you try to import anything that is not a .csv file, the app will just unceremoniously drop dead. Have a look here for more info on how to ensure that it’s a .csv file. )
We have the data in, what do we want to do with it?
As a first step, we want to update the selections in the inputs section — as of now, the only option is “Not Selected”.
We are going to use two new concepts:
The whole thing is going to be wrapped up in an observeEvent. This function lets you execute a piece of code when a specific input field or reactive expression changes. The thing that we are going to look out for now is going to be the data_input reactive expression.
In order to dynamically change the selection, we use updateSelectInput. This function, as the name suggests, takes a selectInput field, and updates something about it, the choices in our case.
We also create a variable, choices, that is going to be a list of
“Not Selected”, which we always want at the top,
and the columns in the data.table.
The relevant code to be entered in the server function:
observeEvent(data_input(),{  choices <- c(not_sel,names(data_input()))  updateSelectInput(inputId = “num_var_1”, choices = choices)  updateSelectInput(inputId = “num_var_2”, choices = choices)  updateSelectInput(inputId = “fact_var”, choices = choices)})
(Note: I initially used a very weird solution here to update the selections. There is an option to add output fields in your ui that will then be rendered as input widgets, with the help of the (very confusingly named) renderUI function. So you determine the position of the ui element in the ui section, but then it’s dynamically created as an output. I found this solution to be confusing, and not very logical, because the nice and clear distinction between layout and content became blurry, but it’s a possibility.)
We should see the impacts of importing a .csv file now, once it’s in, you should be able to select the column names in the dropdown lists:
Now that we can select which columns to use, time to connect them to the output.
Let’s try to imagine what we want to see when we change one of the dropdown selections. The answer is: we don’t actually want to see any changes. It would be quite annoying to see the plot jumping before we finished our selections. We need a barrier between the inputs and the outputs, and that is what the Run Analysis button is for.
We are going to use an eventReactive expression. It’s like a regular reactive expression, but only updates depending on the change of another input.
num_var_1 <- eventReactive(input$run_button,input$num_var_1)num_var_2 <- eventReactive(input$run_button,input$num_var_2)fact_var <- eventReactive(input$run_button,input$fact_var)
With this, we created three expressions, with names identical to the input sources (a naming convention you might find annoying, but I found it to be cleaner this way). The trick is that the num_var_1() expression will only update its value to the latest num_var_1 input when there is a change at the run_button input.
And a change to a button is going to be a click 10 times out of 10.
(Note: These values are going to be character values, but there is no need to use renderText, because we are not actually directly putting them on the screen.)
First, we are going to need to create a plot object. Just like the variables above, this will depend on the button click. Why can’t we just rely on the value updates? We are going to use 4 variables: data_input(), num_var_1(), num_var_2(), and fact_var(). The issue is that data_input() is not protected by a button click, and whenever we select a new file, the value updates.
So this is what we are going to have:
plot_1 <- eventReactive(input$run_button,{    draw_plot_1(data_input(), num_var_1(), num_var_2(), fact_var())  })
Where draw_plot_1 is a regular R function, and it can be defined outside of the server function. For a full version of the draw_plot_1 function, have a look at my GitHub. The more I tried to be clever with building theggplot block by block, the less readable the code got, so I stayed with an unelegant long if-else statement. The function starts like this:
draw_plot_1 <- function(data_input, num_var_1, num_var_2, fact_var){  if(num_var_1 != not_sel &      num_var_2 != not_sel &      fact_var != not_sel){     ggplot(data = data_input,         aes_string(x = num_var_1,y = num_var_2,color = fact_var)) +        geom_point()    }
So the function takes three inputs, a data.table (observe that it is no longer a function inside the draw_plot_1 function, but really a regular table, so no need to use brackets!), and three column names, any of which can be “Not Selected”, in which case it should be ignored.
In the first branch shown above, all the variables are used. In this case, we want a scatter plot where the two axes are the two numerical variables, and they are split into groups by the factor variable.
Of course, you can change these plots however you see fit for your purposes!
Finally, we have to render the plot inside the server function:
output$plot_1 <- renderPlot(plot_1())
If you run the code now, you should be able to see updates to the plot depending on which variables you selected. If all three variables are selected, you should have a colourful scatter plot:
(Not that you would ever want to plot this many factors by colour!)
And if you only have one numerical variable, it should draw a violin plot:
The way we create the tables is going to use similar logic.
Recall that we put in some titles for the first three tables. We can define their values like so:
output$num_var_1_title <-   renderText(paste("Num Var 1:",num_var_1()))output$num_var_2_title <-   renderText(paste("Num Var 2:",num_var_2()))output$fact_var_title <-   renderText(paste("Factor Var:",fact_var()))
No need to put these inside an eventReactive and make the update conditional on the action button, because the underlying variables are already only updating when the action button is clicked on. Just to reiterate, for example, the num_var_1() expression that we use in the formula above is this one:
num_var_1 <- eventReactive(input$run_button,input$num_var_1)
and not the input$num_var_1 itself.
If you run the app now, you should see the headers (recall that we put in “Combined Statistics” as a fixed text when we created the UI):
We have four tables to create, let’s see how the 1-dimensional numerical table is created, the rest follow a similar structure, check out the code in GitHub.
First, we use another eventReactive expression to run a function whenever the action button is clicked on:
num_var_1_summary_table <- eventReactive(input$run_button,{    create_num_var_table(data_input(), num_var_1())  })
Next, we define the create_num_var_table function:
create_num_var_table <- function(data_input, num_var){ if(num_var != not_sel){  col <- data_input[,get(num_var)]  if(length(col)>5000) col_norm<-sample(col,5000) else col_norm<-col  norm_test <- shapiro.test(col_norm)  statistic <- c("mean", "median", "5th percentile",                  "95th percentile", "Shapiro statistic",                  "Shapiro p-value")  value <- c(round(mean(col),2), round(median(col),2),             round(quantile(col, 0.05),2),              round(quantile(col, 0.95),2),             norm_test$statistic, norm_test$p.value)  data.table(statistic, value)  }}
The result is going to be a data.table with two columns called statistic and value. I just put on a few statistics I could think of, along with a normality test, add whatever you feel like.
(Note: the built-in normality test has a 5,000 sample size limit, so I took a random sample if the imported .csv file turns out to be longer.)
Finally, we put the table on the screen by rendering it:
output$num_var_1_summary_table <- renderTable(num_var_1_summary_table(),colnames = FALSE)
I did not want to show the headers of the tables, this can be set in the colnames parameter.
You need to follow a similar structure for the other tables. In the combined table, just like we did with the plot, there are different branches, based on what kind of variables we have selected. If we have all three variables selected, the table will calculate the correlation between the two numerical variables in the different subgroups by the factor variable.
Needless to say, this is just one approach, you can literally put anything you want in these tables once the structure is done.
If you run the app, this is what you should see:
Aaand, we’re done, right? Almost!
As the everlasting advice says, never leave anything on default!
So we are going to change the default theme. There are several ways to do that, including using your own CSS template, we are going to take a very simple approach, and use shinythemes. You will need to install the package first. Then, all you need to do is pick a theme, and put it in the ui object like so:
theme = shinytheme('united')
And voila, we can see the same screen, but it’s orange now (or whatever you picked):
Now we’re done!
This guide turned out to be much longer than I anticipated, there are a few nuances that I did not feel I should mention, but for completeness’ sake, here they are:
As a default, the max file size you can upload is 5 MB. You can increase that by entering this code in the server function:options(shiny.maxRequestSize=10*1024^2)(this will be 10 MB)
If you want to use number fields as factors, R will generate the plot but add a continuous colour scale. In the draw_plot function, I started with this code to convert the column to a factor:  if(fact_var!=not_sel){ data_input[,(fact_var):= as.factor(data_input[,get(fact_var)])] }
You might have noticed that we are using eventReactive a lot for the same button click. It might have been nicer to collect everything in a data.frame rather than keeping them in separate events. And the plot and table drawing functions also use an eventReactive, even though most of the variables are already “eventReactivated” inside. The reason why I did that was because data_input() updates every time a new .csv file is imported. It might be clearer to add another data_input_2() variable instead, which updates based on data_input() when you click on the button and then use that function in regular reactive expressions along with num_var_1() etc. This is just one example of how this reactive programming can be very dynamic but confusing at the same time.
When it comes to actually run the app, you have three options.
You can run it locally, for example by clicking on the Run App button in RStudio. You can also run it without opening the script itself with the runApp function.The second option is Shiny Server, with which you can host it on your own server. I have no experience with this option.Or you can use shinyapps.io, which is the cloud service.
You can run it locally, for example by clicking on the Run App button in RStudio. You can also run it without opening the script itself with the runApp function.
The second option is Shiny Server, with which you can host it on your own server. I have no experience with this option.
Or you can use shinyapps.io, which is the cloud service.
I used shinyapps.io to host the app, you can check it out here.
Using shinyapps.io is really straightforward, you sign up, create an account, get a token with which you can publish your app. With a free account, you get 5 apps and 25 active user hours per month.
matepocs.medium.com
Once again, I recommend Mastering Shiny by Hadley Wickham if you want to learn Shiny in a nice and structured way:
mastering-shiny.org
You can find some handy Shiny cheatsheets here:
www.rstudio.com
And this is a GitHub repo with a list of R packages that enhance Shiny’s default functionality: