Shiny is a web application framework for R, produced by RStudio.

A Shiny app usually has two files, server.R and ui.R. These take care of the web server backend and the HTML frontend, respectivily.

To run a Shiny app you need to have a Shiny server running. RStudio comes with one pre-installed for running your apps locally, but for publishing you will need to install Shiny server or host via shinyapps.io.

Shiny apps use a functionality called reactivity that means that apps can be quick and responsive to changes to inputs - this is one of the best features of Excel, where changing one cell can have consequences throughout the Workbook.

Shiny apps can be tricky to get your head around due to its different work flow from normal R programs, its recommended to go through the tutorials online.

Before you begin

The Shiny website is the source of all that will be said here today, for deeper knowledge refer to that.

We will go through today some practical experience that will hopefully smooth off the learning curve, with a digital marketing focus.

Overview

Shiny uses reactive programming. What does that mean?

ui.R

The ui.R is the equivalent of a website frontend. It contains all the HTML, CSS and JavaScript of a normal webpage. As such its usually easier to get to grips with.

All the most HTML tags are available using shiny::tags(), with shortcuts to the most common e.g. div()

The most minimal UI you can make looks like this:

## in ui.R
shinyUI(
  fluidPage(
    ## output functions
)

The functions shinyUI and fluidPage create the minimal HTML necessary for a Shiny app:

library(shiny)
## 
## Attaching package: 'shiny'
## The following object is masked from 'package:pander':
## 
##     p
shinyUI(fluidPage())

You will need to deal with a lot of brackets. Be stringent in knowing which closing brack refers to which function, as it can be very hard to debug. Good code style indentation helps:

library(shiny)
shinyUI(
  fluidPage(
    
  ) ## end fluidPage
) ## end shinyUI

Building a UI is then a matter of building up the structure in lots of nested UI functions.

Interaction with outputs

Most UI elements that will work with the server.R are called blahOutput().

Behind the scenes this sets up a div tag with the ID of the element that will be changed dynamically via server.R:

## in ui.R
plotOutput("example")
## <div id="example" class="shiny-plot-output" style="width: 100% ; height: 400px"></div>

server.R interacts with this div ID via a named lists called output - it outputs changes to this ID. If the ID is not there, you will see nothing.

The ID must match the list name, and the type must be the same via the renderBlah() server functions e.g. plotOutput in ui.R, renderPlot in server.R

## in server.R
output$example <- renderPlot({
  ## a function that creates a plot
  plot(mtcars)
})

The above uses output$example - this will output to plotOutput("example")

Working with inputs

Likewise, all the input IDs are available to server.R via a named list called input.

The input elements in ui.R are all called something like blahInput e.g. sliderInput()

Again, you specify an ID for that input, but also criteria on what a user can input:

library(shiny)

## create a slide input
sliderInput("input1", label = "A slider", min = 1, max = 10, value = 2)

# <div class="form-group shiny-input-container">
#  <label class="control-label" for="input1">A slider</label>
#  <input class="js-range-slider" id="input1" data-min="1" data-max="10" data-from="2" data-step="1" 
# data-grid="true" data-grid-num="9" data-grid-snap="false" data-prettify-separator="," data-keyboard="true" 
# data-keyboard-step="11.1111111111111" data-drag-interval="true" data-data-type="number"/>
# </div>

The output is more complicated than the blahOutput functions, but again it is only HTML.

To refer to the input elements, server.R then refers to a named list called input:

input$input1

Inputs effect outputs, and hence we get interactivity.

A very basic minimal example below:

## ui.R
library(shiny)

shinyUI(
  fluidPage(
    textInput("text_input", label = "input text"),
    textOutput("text_output")
  )
)
# server.R
library(shiny)

function(input, output) ({
   
  output$text_output <- renderText({
    input$text_input
  })
  
})

All that said, its best to start with ui.R first to design your UI, then work on the server.R to output what you need.

server.R

server.R contains the code that will change certain elements defined in the ui.R.

server.R is in fact a constantly looping function. As a user interacts with the ui.R it sends messages back to the server.R which then reacts (see, reactivity), outputs data to change and sends this back to the ui.R that renders it on the correct ID.

The looping function is around the objects we spoke about earlier - input and output. Optionally, you can also include an object called session which contains information about the general user session (such as what URL is being shown), which starts to explain how a minimal server.R file looks:

## in server.R
library(shiny)

function(input, output){

  output$plot1 <- renderPlot({
  
    ## get the data from csv
    web_data <- read.csv("gadata_example_2.csv", stringsAsFactors = FALSE)
    plot(web_data$date, web_data[[input$value1]])
  
  })

}

Exercise

  1. What will this Shiny app render? Assume input$value1 is the name of a metric column of web_data

  2. Make a Shiny app in a new RStudio project. (Click New > Shiny > Multiple File in RStudio interface) construct the ui.R that will alter the server.R code above. You will need to use the functions selectInput() to choose the data column you want to plot and plotOutput() to output the result.

  3. What steps should we take to use highcharter instead of base plot?

Use these functions - renderHighchart, highchartOutput and to make the plot with the below:

library(highcharter)
hchart(web_data, "line", hcaes(x = date, y = sessions))

Reactive objects

Because its a looping function, standard R rules about scoping don’t apply. You need to deal with functions like reactive(), observe(),isolate(), eventReactive() and observeEvent() which control behaviour in this looping function.

Key to this are a new class of object called reactive objects. These are objects that will change according to user input, and as such can’t be treated as normal R variables.

Reactive object can react to another reactive object, which sets up a chain of dependencies. A change in a dependency will effect all other reactive objects. A key factor in making efficient Shiny apps is to know when to trigger a change and when not to.

Using Reactives example

The below builds on the app below, but we now also want to display the Chi Squared value. As we will now have two UI elements we will have two server.R calls that rely on the same data.

Instead of importing the data in twice (it could be a API call) we will make a reactive object, and then when that object is ready (e.g. not NULL) pass it to the functions.

The req()function serves as a roadblock to stop the plot and text firing before the data is ready (important for API calls)

library(shiny)
library(dplyr)

function(input, output){
  
  ## all ui output rely on this data, but it is only ever called once
  raw_data <- reactive({
    
    ## get the data from csv
    read.csv("../data/gadata_example_2.csv", stringsAsFactors = FALSE)
    
  })
  
  ## transforms the raw data so it is suitable for chi square
  chi_data <- reactive({
    
    req(raw_data())
    req(input$metric)
    req(input$device)
    req(input$channel)
    
    raw_data <- raw_data()
    
    metric_data <- raw_data[, c("channelGrouping","deviceCategory", input$metric)]
    metric_data$metric <- metric_data[[input$metric]]
    
    metric_data <- metric_data[metric_data$deviceCategory %in% input$device, ]
    metric_data <- metric_data[metric_data$channelGrouping %in% input$channel, ]
    
    metric_data %>% 
      select(channelGrouping, deviceCategory, metric) %>%
      group_by(channelGrouping, deviceCategory) %>% 
      summarise(metric_sum = sum(metric)) %>% 
      tidyr::spread(channelGrouping, metric_sum)
    
  })
  
  output$table <- renderTable({
    req(chi_data())
    
    chi_data()
  })
  
  output$chi_text <- renderText({
    
    ## this is a roadblock so this function won't fire until the_Data is non-NULL
    req(chi_data())
    req(input$device)
    req(input$channel)
    
    ## do this as it make debugging a lot easier
    chi_data <- chi_data()

    chi_test <- chisq.test(chi_data[-1])
    paste("NULL hypothesis there is no relationship between ", paste(input$device, collapse = ", "), 
          " and ", 
          paste(input$channel, collapse = ", "),
          " - ",
          chi_test$method, " Chi^2: ", chi_test$statistic, " : p-value ", chi_test$p.value)
    
  })
  
}

The req(input$device) is good practice to have as the input list are not necessarily available to the function on start up - this guards against ugly red messages briefly when the app starts up.

With a ui.R adding the max text:

library(shiny)
library(highcharter)
shinyUI(
  fluidPage(
    selectInput("metric", label = "select column", choices = c("sessions",
                                                               "pageviews",
                                                               "entrances",
                                                               "bounces")),
    selectInput("device", label = "select device", choices = c("desktop", "mobile", "tablet"), multiple = TRUE),
    selectInput("channel", label = "select channel", choices = c("(Other)","Direct","Display","Email","Organic Search","Paid Search","Referral","Social","Video"), multiple = TRUE),
    tableOutput("table"),
    textOutput("chi_text"),
    br()
  )
)

Using reactive() Exercise

Add a plot to the Shiny app displaying some trend data, using the same raw_data() reactive source above. You can use some helper code shown below if you like:

library(highcharter)
library(dplyr)

## get the data from csv
plot_data <- raw_data()

## filter down to choices
plot_data <- plot_data[plot_data$channelGrouping %in% input$channel, ]
plot_data <- plot_data[plot_data$deviceCategory %in% input$device, ]

## trick to make easier to work with dplyr
plot_data$metric <- plot_data[[input$metric]]

## aggregate on metric
plot_data %>% 
  select(date, metric) %>% 
  group_by(date) %>% 
  summarise(metric_sum = sum(metric))

## create trend plot
hchart(plot_data, "line" , hcaes(x = date)) %>% hc_add_series(plot_data$metric_sum)

Shiny Modules

Its possible to package up common UI elements into a Shiny module. This can let you repeat templated code.

An example comes with googleAnalyticsR to let you create a login for users. The below is from the documentation.

## in server.R
library(googleAuthR)
library(googleAnalyticsR)
library(shiny)
library(highcharter)

function(input, output, session){
  
  ## Get auth code from return URL
  token <- callModule(googleAuth, "login")
  
  ga_accounts <- reactive({
    req(token())
    
    with_shiny(ga_account_list, shiny_access_token = token())
    
  })
  
  ## get the ViewId from what the users select in the dropdown
  selected_id <- callModule(authDropdown, "auth_menu", ga.table = ga_accounts)
  
  gadata <- reactive({
    
    req(selected_id())
    gaid <- selected_id()
    
    ## with shiny needed to read the correct token
    with_shiny(google_analytics,
               id = gaid,
               start=input$date_select[1], end=input$date_select[2], 
               metrics = c("sessions"), 
               dimensions = c("date"),
               shiny_access_token = token())
  })
  
  output$something <- renderHighchart({
    
    ## only trigger once authenticated
    req(gadata())
    
    gadata <- gadata()
    
    ## creates a line chart using highcharts
    hchart(gadata, "line" , hcaes(x = date, y = sessions))
    
  })
  
}

The modules are used below to render the dropdowns and login buttons:

## ui.R
library(googleAuthR)
library(googleAnalyticsR)
library(shiny)
library(highcharter)

shinyUI(
  fluidPage(
    googleAuthUI("login"),
    authDropdownUI("auth_menu"),
    dateRangeInput("date_select", "Select Date", start = Sys.Date() - 30),
    highchartOutput("something")
    
  ))