I’m going to make a shiny app of a few of my previous charts in this challenge for interactive day. I’ll be using a lot of resources from my book,
Setup
#remotes::install_github("debruine/shinyintro")library(tidyverse) # for data wrangling and vizlibrary(sf) # for mapslibrary(rnaturalearth) # for map coordinateslibrary(ggthemes) # for map themelibrary(lwgeom) # for map projectionlibrary(shinyintro) # for shiny app templatelibrary(tictoc) # for timing thingslibrary(tidytext) # for text analysislibrary(topicmodels) # for topic modellinglibrary(MetBrewer) # for beautiful colourblind-friendly colour schemes
26.1 Set up shiny app
You can do all this manually, but my shinyintro companion package to the book has a function to set up the skeleton of an app exactly how I like it.
Code
shinyintro::clone(app ="basic_template", dir ="app")
Next, I’ll edit the README.md and DESCRIPTION files to describe my app and update the app.R file for my new app structure. The skeleton app code is for a tabbed interface using shinydashboard. I’ll set the skin to purple, change the demo_tab to an intro_tab and add a tab for day6, the first chart I’m going to make interactive.
Tabs Code
## tabs ----intro_tab <-tabItem(tabName ="intro_tab",p("For Day 26: Interactive, I'm going to make interactive versions of a few of the charts I made for previous days of the 30DCC."),img(src ="img/30DCC.png", width =500, height =500))day6 <-tabItem(tabName ="day6")
I like to define my tabs as objects so I can easily move them to a separate file and source them in when the amount of code gets overwhelming. I also have to add menuItems for these tabs to the dashboardPage() sidebar, find icons at fontawesome, and add the tab objects into tabItems() in the dashboardBody().
UI Code
## UI ----ui <-dashboardPage(skin ="purple",dashboardHeader(title ="30-Day Chart Challenge 2022", titleWidth ="calc(100% - 44px)"# puts sidebar toggle on right ),dashboardSidebar(# https://fontawesome.com/icons?d=gallery&m=freesidebarMenu(id ="tabs",menuItem("Intro", tabName ="intro_tab", icon =icon("calendar")),menuItem("Day 6: OWiD", tabName ="day6", icon =icon("globe")) ) ),dashboardBody( shinyjs::useShinyjs(), tags$head(# links to files in www/ tags$link(rel ="stylesheet", type ="text/css", href ="basic_template.css"), tags$link(rel ="stylesheet", type ="text/css", href ="custom.css"), tags$script(src ="custom.js") ),tabItems( intro_tab, day6 ) ))
This is what the app looks like at this point when you run it:
26.3 Day 6
This was originally an animated map showing the share of the population using the internet in every country from 1990 to 2019. My plan is to make this interactive by adding a slider so you can pick the year.
26.3.1 Map functions
I like to keep as much code as possible out of the app.R file, so I’m going to add some functions to the scripts/func.R file to create the main plot. First, I need to create the dataset to be filtered.
Nope, reading the data isn’t adding much to the plotting time, but half a send faster is still better. Maybe we’ll come back to this. Add the rest of the plot functions.
I need to add an input and an output to the day 6 tab. Set sep = "" to remove the comma for the thousands separator, which makes no sense for years.
Day 6 UI
day6 <-tabItem(tabName ="day6",sliderInput("day6_year", label ="Year", min =2009, max =2019, value =2019,step =1, sep =""),plotOutput("day6_plot"))
26.3.3 Server function
I also need to add the code to produce the output plot to the server function. I defined the data table day6_data outside the server function because this only needs to run once, when the app in initialised. There’s no reason to run it for every instantiation of the app or every time the plot is rendered.
Now I just need to add some title and caption text. It’s still a bit slow, but properly interactive. If this is all I needed it to do, I’d probably pre-render the chart for each of the 11 years at app startup and flip between the images rather than rerender the plot each time, but this is fine for today’s challenge.
26.4 Day 18
Day 18 was one where I had to look at a few different versions of the code and plot before deciding on some parameters, so let’s make those topic modelling parameters interactive.
Now add the function to scripts/func.R and make sure to add the appropriate packages to the top of the app.R file. Add dtm <- dtm() to the top of the app.R script (after sourcing in func.R) because this only needs to be called once. Make sure to move oecd_covid_insights.rds to the app data folder.
26.4.4 UI
Same as above, make a new tab for day 18, with inputs and an output plot.
Day 18 UI
day18 <-tabItem(tabName ="day18",h2("LDA Topic Analysis of OECD Data Insights"),numericInput("day18_k", label ="Number of Topics",min =2, max =16, value =6, step =1),numericInput("day18_topterms", label ="Number of Terms per Topic",min =1, max =20, value =7, step =1),textAreaInput("day18_topics", label ="Topic Labels (one per line)",rows =6),plotOutput("day18_plot"),HTML("<caption>Data from <a href='https://www.oecd.org/coronavirus/en/data-insights/'>OECD</a> | Plot by <a href='https://twitter.com/lisadebruine'>@lisadebruine</a></caption>"))
After a few tests, I realised that I needed to remove the constraint of nrow = 2 in the facet_wrap() and also restructure how I was splitting the string in the topic labels to deal with blank lines or too few/many labels.
I also decided it was too annoying waiting for the whole plot to update every time I wanted to change the topic labels, so restructured the server code to only rerun code when absolutely necessary, and to add an update button for the topic labels, so they only update when you’re done editing the labels.
The reactive() functions only run when the inputs or reactive functions they contain update, while the observeEvent() function only runs when the input value for the first argument changes.
I currently don’t have access to our work shiny server because of some server changes, so I deployed the app at debruine.shinyapps.io/30DCC.
It didn’t work the first time, and I realised that I only use one function each from rnaturalearth and lwgeom, so I saved world as an RDS object and loaded it from the saved file instead of using those packages.
That stopped the total app failure, but the day18 chart still wouldn’t build, so I added debug messages to the day18 functions so I could see what was and wasn’t running.
The debug_msg() function is a custom function that prints the debug message in the console during development, and in the javascript console when deployed (in case you don’t have access to the logs). The code below prints “day18_topterms” whenever the tt() reactive function runs, and if there is an error in the code, tryCatch() catches it and prints the error message with debug_msg(). This way, I discovered that I was getting an error “there is no package called ‘reshape2’”, so I added that to the library() calls at the start of the script.