class: center, middle, inverse, title-slide # Making a Shiny application ##
https://kevinwang09.github.io/shiny_3888/
###
Kevin Y. X. Wang
### 2020 April 02, slides compiled on 2020 Apr 02 --- # Instructions + All materials are in this repo: https://github.com/kevinwang09/shiny_3888 + Try to download a copy of the repository via (https://github.com/kevinwang09/shiny_3888/archive/master.zip) if they want to run the materials interactively in class. + Slides are here: https://kevinwang09.github.io/shiny_3888 + Install revevant packages here: https://github.com/kevinwang09/shiny_3888/blob/master/install.R --- # Lecture outline + Case scenario: Sydney airport dashboard in a Rmd file + Inputs and outputs of a Shiny app + Demo 1: `bomrang` Shiny app - wind plot + Demo 2: `bomrang` Shiny app - temperature plot and `reactive` + Demo 3: `bomrang` Shiny app - deployment + COVID-19 Shiny app competition + Tips and resources for Shiny --- class: segue Motivation: weather warning --- # Case study: issuing wind warnings + Imagine that you are working at Sydney airport as a data scientist + You are responsible for issuing wind speed warnings whenever the windspeed reaches 30km/h (or above) in the past 72 hours <center> <img src="figures/SYD_wind_warning.png", width="50%"> </center> --- # `bomrang.Rmd` [Vignette of bomrang](https://cran.r-project.org/web/packages/bomrang/vignettes/bomrang.html) .scroll-output[ ```r library(bomrang) library(tidyverse) location = "Sydney airport" weather = bomrang::get_current_weather(location) glimpse(weather) ``` ``` ## Observations: 144 ## Variables: 35 ## $ sort_order <int> 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14… ## $ wmo <int> 94767, 94767, 94767, 94767, 94767, 94767, 94767,… ## $ full_name <chr> "Sydney Airport", "Sydney Airport", "Sydney Airp… ## $ history_product <chr> "IDN60801", "IDN60801", "IDN60801", "IDN60801", … ## $ local_date_time <chr> "02/03:30pm", "02/03:00pm", "02/02:30pm", "02/02… ## $ local_date_time_full <dttm> 2020-04-02 15:30:00, 2020-04-02 15:00:00, 2020-… ## $ aifstime_utc <dttm> 2020-04-02 04:30:00, 2020-04-02 04:00:00, 2020-… ## $ lat <dbl> -33.9465, -33.9465, -33.9465, -33.9465, -33.9465… ## $ lon <dbl> 151.1731, 151.1731, 151.1731, 151.1731, 151.1731… ## $ apparent_t <dbl> 22.8, 23.0, 22.2, 21.2, 21.3, 22.4, 21.6, 22.5, … ## $ cloud <chr> "Mostly clear", "Cloudy", "Cloudy", "Cloudy", "C… ## $ cloud_base_m <dbl> 1200, 1500, 2880, 2880, 2940, 1200, 1200, 600, 6… ## $ cloud_oktas <dbl> 1, 8, 8, 8, 8, 1, 1, 6, 1, 1, 1, 1, 1, 7, 1, 1, … ## $ cloud_type <chr> "Cumulus", "-", "-", "-", "-", "Cumulus", "Cumul… ## $ cloud_type_id <int> 8, 31, NA, NA, NA, 8, 8, 32, 8, 7, 8, 8, 7, 36, … ## $ delta_t <dbl> 2.2, 3.2, 3.7, 3.8, 4.4, 4.7, 4.7, 4.8, 4.9, 4.9… ## $ gust_kmh <int> 17, 26, 26, 28, 32, 30, 30, 28, 32, 28, 35, 37, … ## $ gust_kt <int> 9, 14, 14, 15, 17, 16, 16, 15, 17, 15, 19, 20, 1… ## $ air_temp <dbl> 22.4, 23.6, 23.7, 23.3, 23.7, 24.3, 24.3, 24.4, … ## $ dewpt <dbl> 19.0, 18.5, 17.7, 17.1, 16.5, 16.6, 16.6, 16.6, … ## $ press <dbl> 1011.3, 1011.6, 1011.7, 1011.7, 1011.9, 1012.3, … ## $ press_msl <dbl> 1011.3, 1011.6, 1011.7, 1011.7, 1011.9, 1012.3, … ## $ press_qnh <dbl> 1011.3, 1011.6, 1011.7, 1011.7, 1011.9, 1012.3, … ## $ press_tend <chr> "-", "R", "-", "-", "-", "-", "-", "F", "-", "-"… ## $ rain_trace <dbl> 0.4, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0… ## $ rel_hum <int> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, … ## $ sea_state <chr> "-", "-", "-", "-", "-", "-", "-", "-", "-", "-"… ## $ swell_dir_worded <chr> "-", "-", "-", "-", "-", "-", "-", "-", "-", "-"… ## $ swell_height <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, … ## $ swell_period <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, … ## $ vis_km <chr> "8", "15", "10", "10", "10", "10", "10", "30", "… ## $ weather <chr> "Rain", "Rain", "-", "-", "-", "-", "-", "Fine",… ## $ wind_dir <chr> "NNE", "NNE", "NNE", "NNE", "NNE", "NE", "NNE", … ## $ wind_spd_kmh <int> 15, 19, 22, 24, 24, 22, 26, 22, 24, 22, 28, 28, … ## $ wind_spd_kt <int> 8, 10, 12, 13, 13, 12, 14, 12, 13, 12, 15, 15, 1… ``` ```r weather %>% ggplot(aes(x = local_date_time_full, y = wind_spd_kmh)) + geom_path() + labs(x = "Time", y = "Wind (km/h)") ``` <img src="index_files/figure-html/unnamed-chunk-2-1.png" width="1008" /> ] --- # Communicating results using a `Rmd` file + A Rmd file captures a snapshot of the data with a fixed set of parameters - Good for reproducibility and record keeping - Bad for looking at updated data (e.g. "Sydney airport" is hard-coded) -- + A Shiny app can access data in real-time and modify outputs (plots & summary statistics) instantly based on user inputs (parameters like "location") + A Shiny app is a dashboard application*. It hides all the computations in the background and only shows inputs and outputs more relevant to the user (a domain expert not familiar with the underlying R codes) .footnote[If you want to see examples of dashboards: https://ourworldindata.org/coronavirus] -- + The purpose of this lecture is to build this simple Shiny app and understand its design: ``` shiny::runGitHub( repo = "shiny_3888", username = "kevinwang09", subdir = "bomrang_3888") ``` --- # Why Shiny feels a bit funny? + A Shiny app is very much point-and-click + But underneath, Shiny uses R for computation and impose very strict rules on input and output + So much so it almost feels like Shiny is a language on its own + For the bomrang Shiny app, try to focus on three components: 1. Input (enter location) 1. Computation (download data, make plots, modelling) 1. Outputs (show figures, make interpretations) --- class: segue Demo 0: Knitting `bomrang.Rmd` will give you a skeleton Shiny app (This is not a full app, but only to see what an "input" and an "output" are in context of a Shiny app) --- # `input` and `output`: the core of Shiny Input: a location as text ```r * textInput(inputId = "location", label = "Put a location here!", value = "Sydney airport") ``` Output: make a ggplot ```r renderPlot({ * weather = get_current_weather(input$location) weather %>% ggplot(aes(x = local_date_time_full, y = wind_spd_kmh)) + geom_path() + labs(x = "Time", y = "Wind (km/h)") }) ``` --- class: segue shiny = `ui.R`+ `server.R` --- # A Shiny app is made up of two `.R` scripts* + Inputs - Special function managing inputs in `ui.R` + Analysis - Write normal `R` codes in `server.R` + Outputs - Special function managing outputs in `ui.R` .footnote[*you are allowed to have more than two scripts, if you wish to better organise substaintial codes (e.g. using `global.R`, see later)] --- class: segue Demo 1: Let's build your first Shiny app! --- # `ui.R` ```r library(shiny) library(bomrang) library(tidyverse) *shinyUI( * fluidPage( textInput(inputId = "location", label = "Put a location here!", value = "Sydney airport"), plotOutput(outputId = "wind_plot") * ) *) ``` + The inputs as determined from `ui.R` are passed to `server.R` as a list, named `input` + You can access these inputs in `server.R` using `input$inputId` + `fluidPage` is not terribly important here --- # `server.R` + The outputs as processed by `server.R` are passed to `ui.R` as a list, named `output` + You can access these ouputs in `ui.R` using `output$outputId` ```r * shinyServer(function(input, output) { * output$wind_plot = renderPlot({ weather = get_current_weather(input$location) weather %>% ggplot(aes(x = local_date_time_full, y = wind_spd_kmh)) + geom_path() + labs(x = "Time", y = "Wind (km/h)") }) }) ``` --- # This is a good time to pause + **Users can interact instantly with the designed input/output. That is its biggest advantage over Rmd**. + My typicall workflow: - Write a `Rmd` file before making a full app to ensure all codes are correct and get a sense of what should be inputs/outputs - Open up a an old Shiny app I made or, from RStudio (File -> New File -> Shiny Web App) -- + There are many more `render*` and `*Input` functions, see [here](https://kevinwang09.github.io/SSA-Shiny-Workshop-2019/01-Intro.html#20) for a incomplete list. + Designing a good UI is more of an art, I found looking at other people's work to be quite useful (see extra links at the end of these slides) --- class: segue Demo 2: `reactive`, a key concept in Shiny apps --- # "reactive" values + `renderPlot` (and other types of `render*`) are called "reactive" functions. Meaning they will react to changes in the `input` list. + The output of a reactive function is a reactive value. The reactive function will get re-evaluated whenever there is a change in the `input`, so the reactive value will change accordingly. + Trying to make an output without a reactive function (i.e remove the `renderPlot` function) results in an error similar to this: ``` Error in .getReactiveEnvironment()$currentContext() : Operation not allowed without an active reactive context. (You tried to do something that can only be done from inside a reactive expression or observer.) ``` --- # This `server.R` will fail. Why? ```r shinyServer(function(input, output) { output$wind_plot = renderPlot({ weather = get_current_weather(input$location) weather %>% ggplot(aes(x = local_date_time_full, y = wind_spd_kmh)) + geom_path() + labs(x = "Time", y = "Wind (km/h)") }) * output$temp_plot = renderPlot({ * weather %>% * ggplot(aes(x = local_date_time_full, * y = apparent_t)) + * geom_path() + * labs(x = "Time", * y = "Apparent Temp (Celcius)") * }) }) ``` --- # This `server.R` will fail. Why? ```r shinyServer(function(input, output) { output$wind_plot = renderPlot({ * weather = get_current_weather(input$location) * weather %>% ggplot(aes(x = local_date_time_full, y = wind_spd_kmh)) + geom_path() + labs(x = "Time", y = "Wind (km/h)") }) output$temp_plot = renderPlot({ * weather %>% ggplot(aes(x = local_date_time_full, y = apparent_t)) + geom_path() + labs(x = "Time", y = "Apparent Temp (Celcius)") }) }) ``` --- # This `server.R` will work, but why it is a bad idea? ```r shinyServer(function(input, output) { output$wind_plot = renderPlot({ * weather = get_current_weather(input$location) weather %>% ggplot(aes(x = local_date_time_full, y = wind_spd_kmh)) + geom_path() + labs(x = "Time", y = "Wind (km/h)") }) output$temp_plot = renderPlot({ * weather = get_current_weather(input$location) weather %>% ggplot(aes(x = local_date_time_full, y = apparent_t)) + geom_path() + labs(x = "Time", y = "Apparent Temp (Celcius)") }) }) ``` --- # The `reactive()` function + Since the two plots has the same reactive component, i.e. fetching the data via the line ``` weather = get_current_weather(input$location) ``` we only need make a general function that will react to the location input and pass that onto the two `renderPlot` functions. + This is where the `reactive()` function comes in! --- # `reactive()` in action ```r shinyServer(function(input, output) { * fetch_data = reactive({ * weather = get_current_weather(input$location) * return(weather) * }) output$wind_plot = renderPlot({ fetch_data() %>% ggplot() ## etc }) output$temp_plot = renderPlot({ fetch_data() %>% ggplot() ## etc }) }) ``` --- class: segue Demo 3: more advanced reactive actions --- # `actionButton` and `eventReactive` + When we type out the phrase "Sydney airport", you notice the plots change immediately. + However, this actually isn't ideal: multiple fetches of the data will be executed as your type out the letters. + This could be a problem if the computation is really computationally intensive or makes too many API requests to an external server. + You could use an `actionButton()` in the `ui.R`, which, upon clicking, changes the value of `input$button`. + `eventReactive` in `server.R` will be set up such that every change in the value of `input$button` will trigger the execution of some codes. --- # `actionButton` and `eventReactive` + `ui.R` ```r shinyUI(fluidPage( ... * actionButton(inputId = "button", label = "Fetch data and plot") ... )) ``` + `server.R` ```r shinyServer(function(input, output) { * fetch_data = eventReactive(input$button, { weather = get_current_weather(input$location) return(weather) }) ... ## renderPlot as before }) ``` --- # `observe()` changes the internal values objects (extension) + Unlike `reactive()`, the `observe()` will change the internal values of `R` objects. + Try adding this code to `server.R` and see what happens in the **console**. ```r obs = observe({ print( paste0(input$location, " is fun!") ) }) ``` + A special case of `observe()` is `observeEvent()`, see some uses in context of `actionButton()` [here](https://shiny.rstudio.com/articles/action-buttons.html). --- # `global.R` will always run when you launch a Shiny app + A good place to put in codes like: ```r library(shiny) library(bomrang) library(tidyverse) library(forecast) theme_set(theme_bw(base_size = 18)) customised_function = function(...){ ... } ``` --- class: segue Demo 4: deploying Shiny app --- # Deploying your app + You can share your app via `shinyapps.io`. - Hosts up to 5 applications and 25 active hours per month. + Alternatively, you can put the Shiny scripts on GitHub. - People need to run the app locally on their laptop with all necessary packages installed. ```r library(tidyverse) library(shiny) library(forecast) library(ggrepel) library(nCov2019) ## devtools::install_github("GuangchuangYu/nCov2019") shiny::runGitHub( repo = "covid19", username = "kevinwang09", ref = "master") ``` --- class: segue Demo 5: debugging Shiny app (extension) --- # Empty string in the input text box See https://shiny.rstudio.com/articles/debugging.html + Most common error: unexpect input. Put `validate()` and `need()` around the appropriate input ``` validate( need(input$location, 'Choose a location!')) ``` + Set breakpoint + `browser()` + `shiny::runApp(display.mode="showcase")` + `shiny::showReactLog()` + Print warning messages in console for diagnosis + `options(shiny.error = browser)` --- # Summary 1. Shiny is a R framework to build dashboard applications 2. Shiny places great emphasis on the inputs and outputs of an app and use R as the backend for computation 3. `reactive` is a general function that reacts to changes in `input` 4. `eventReactive` allows for a delayed action in the app (together with the use of `actionButton`) --- # Do's and dont's - Do's + Write a Rmd file to plan out what are the inputs/ouputs of an app + Build a working app first before prettifying + Google! There are a lot of free resources/blog posts and app examples on GitHub + Make lots of git commits (always have a working copy to go back to) - Dont's + Write awfully long functions, try to modularise your code whereever possible (this is why I don't use `app.R`) + Manage your brackets by using comments and chunk collapse + Cry when debugging Shiny + Make an app that takes up a lot of resources --- # Talking through some apps + [COVID19](https://github.com/kevinwang09/covid19): - `tabset` and gradually improved UI in the commit history - Modularised functions + [Cats and Dogs](https://github.com/kevinwang09/catdog_3888): - Loading a pre-trained keras model in `global.R` - Similar website: https://isthisacat.com/ + Other apps --- # Shiny app submission (not assessed, more for practising your skills) + Submit an app analysing COVID19 data here: https://github.com/kevinwang09/shiny_3888/issues/1 --- # Some resources + A full set of official tutorial materials: https://shiny.rstudio.com/tutorial/ + [Dr Wishart's workshop on Shiny](https://github.com/jrwishart/SSA-Shiny-Workshop-2019), which I enabled the slides [here](https://github.com/kevinwang09/SSA-Shiny-Workshop-2019). + [RStudio workshop on Shiny](https://github.com/rstudio-conf-2020/shiny-start-finish) + A massive collection of Shiny apps: https://github.com/mkearney/shinyapps_links + Gallery of Shiny apps: https://shiny.rstudio.com/gallery/ + 2020 Shiny app contest: https://community.rstudio.com/tags/shiny-contest-2020 --- <!-- class: segue --> <!-- class: segue-yellow --> ---