Shiny Modules for useful controls

A practical tutorial on Shiny modules — building a moving-average timeseries app with the Online Labour Index data, first without modules, then refactoring to use them.
R
Published

March 25, 2019

Shiny Modules are an “advanced” feature of Shiny apps that developers can use to reduce code duplication, simplify complex inter-relating controls, and allow UI elements to be compartmentalised into R packages.

Much of the stuff written about modules gets lost in the weeds. We’re going to make the Shiny app embedded below, both with and without modules.

The app displays data from the Online Labour Index — a project from the University of Oxford’s Internet Institute studying the online labour market. The data is deposited on Figshare: DOI: 10.6084/m9.figshare.3761562.

Data import and wrangling

Create a script file called data-processing.R:

library("tidyverse")
library("rfigshare")
library("lubridate")

fs_deposit_id <- 3761562
deposit_details <- fs_details(fs_deposit_id)
deposit_details <- unlist(deposit_details$files)
deposit_details <- data.frame(
  split(deposit_details, names(deposit_details)),
  stringsAsFactors = FALSE
)

imported_country_group_data <- deposit_details |>
  filter(str_detect(name, "bcountrydata_")) |>
  pull(download_url) |>
  read_csv() |>
  mutate(timestamp = as_date(timestamp)) |>
  rename(date = timestamp)

gigs_by_country_group <- imported_country_group_data |>
  group_by(date, country_group) |>
  summarise(jobs = sum(count)) |>
  ungroup()

gigs_by_occupation <- imported_country_group_data |>
  group_by(date, occupation) |>
  summarise(jobs = sum(count)) |>
  ungroup()

gg_ma_timeseries functions

Create a file called gg_ma_timeseries.R:

library("ggsci")
library("xts")

gg_ma_timeseries <- function(.data, date, value, category) {
  date <- enquo(date)
  value <- enquo(value)
  category <- enquo(category)

  n_colours <- .data |> pull(!!category) |> unique() |> length()
  colours_from_startrek <- colorRampPalette(pal_startrek(
    palette = c("uniform")
  )(7))(n_colours)

  .data |>
    ggplot(aes(x = !!date, y = !!value, color = !!category)) +
    geom_line() +
    theme_bw() +
    scale_color_manual(values = colours_from_startrek) +
    scale_x_date(expand = c(0.01, 0.01)) +
    scale_y_continuous(expand = c(0.01, 0)) +
    labs(
      title = "Online Labour Index data",
      subtitle = "DOI:10.6084/m9.figshare.376156"
    )
}

ma_job_count <- function(.data, date, value, category, window_width) {
  date <- enquo(date)
  value <- enquo(value)
  category <- enquo(category)
  window_width <- as.numeric(window_width)

  .data |>
    group_by(!!category) |>
    arrange(!!date) |>
    mutate(
      !!value := rollmean(
        !!value,
        k = window_width,
        na.pad = TRUE,
        align = "right"
      )
    ) |>
    filter(!is.na(!!value)) |>
    ungroup()
}

server.R skeleton

library("shiny")
library("tidyverse")
library("rfigshare")
library("lubridate")
library("xts")
library("ggsci")

source("data-processing.R", local = TRUE)
source("gg_ma_timeseries.R", local = TRUE)

function(input, output, session) {}

Shiny app without modules

The module-free version is available at: github.com/charliejhadley/training_shiny-module

usethis::use_course(
  "https://github.com/charliejhadley/training_shiny-module/raw/master/gg_ma_timeseries/shiny-without-modules.zip"
)

The ui.R file duplicates the radioButtons() controls for each tab, and the server.R file duplicates the renderPlot() and renderUI() calls. Modules solve this.

Introducing the Shiny Module

The module has two components:

  • gg_ma_timeseries_input() creates an instance of the controls
  • gg_ma_timeseries_output() creates an instance of the chart

Create /modules/client-side_gg-ma-timeseries.R:

library("shinycustomloader")

gg_ma_timeseries_input <- function(id) {
  ns <- NS(id)
  tagList(
    "This entire tab is a shiny module, including this text, the radio buttons and the chart.",
    fluidRow(column(
      radioButtons(
        ns("landing_rollmean_k"),
        label = "",
        choices = list(
          "Show daily value" = 1,
          "Show 28-day moving average" = 28
        ),
        selected = 28,
        inline = TRUE
      ),
      width = 12
    ))
  )
}

gg_ma_timeseries_output <- function(id) {
  ns <- NS(id)
  withLoader(plotOutput(ns("ma_plot")), type = "html", loader = "dnaspin")
}

Key rules for modules:

  • Place module code in a /modules subfolder.
  • Source input code in ui.R, output code in server.R.
  • ns <- NS(id) ensures you’re referring to the correct input/output namespace.
  • If returning multiple UI elements, wrap them in tagList().

Update ui.R to use the module:

library("shiny")
source("modules/client-side_gg-ma-timeseries.R", local = TRUE)

shinyServer(navbarPage(
  "Shiny Modules",
  tabPanel(
    "By occupation",
    fluidPage(
      gg_ma_timeseries_input("occupation_controls"),
      gg_ma_timeseries_output("occupation_chart")
    )
  ),
  tabPanel(
    "By country",
    fluidPage(
      gg_ma_timeseries_input("by_country_controls"),
      gg_ma_timeseries_output("by_country_chart")
    )
  )
))