Data Table and Plots Not Showing Up (Using Modules)

Hello, I'm new to Shiny and also to this forum. I'm trying to create an app with different pages (one page for a different dataset). Each page would have a tab for a data table and a tab for some plots. I am successful in making it work if it was only one page:

#
# This is a Shiny web application. You can run the application by clicking
# the 'Run App' button above.
#
# Find out more about building applications with Shiny here:
#
#    http://shiny.rstudio.com/
#

# Load necessary libraries
library(shiny)
library(DT)
library(ggplot2)
library(scales)

# Load the data
data <- read.csv("universities.csv", stringsAsFactors = FALSE)


ui_home <- fluidPage(
  titlePanel("Welcome to the BC Post-Secondary School Employee Salaries App"),
  sidebarLayout(
    sidebarPanel(),
    mainPanel(
      h2("About"),
      p("This is a Shiny app that provides insights into the salaries of employees of post-secondary schools in BC."),
      p("The vancouver Sun has collected the names and salaries of workers that earn a yearly salary of more than $75,000 from 
        post-secondary schools in British Columbia."),
      p("Please navigate to the tabs above to explore the data."),
      img(src = "VancouverSun.png", width = "300px", height = "300px")
    )
  )
)

# Define UI for the main app
ui_main <- fluidPage(
  titlePanel("Employee Salaries Data"),
  
  tabsetPanel(
    tabPanel("Table", 
             sidebarLayout(
               sidebarPanel(
                 checkboxInput("sort", "Sort table by selected variable"),
                 selectInput("filter", "Filter by Sector:",
                             choices = c("All", unique(data$Sector))),
                 downloadButton("downloadCSV", "Download CSV")
               ),
               mainPanel(
                 DT::dataTableOutput("table"),
                 textOutput("result_count")
               )
             )
    ),
    tabPanel("Plots",
             sidebarLayout(
               sidebarPanel(
                 h4("Select Plot"),
                 radioButtons("plot_type", "Choose a plot:",
                              choices = c("Average Salary", "Total Salary", "Total Employees")),
                 
                 # Adding a slider input for salary threshold
                 sliderInput("salary_threshold", "Select Salary Threshold:",
                             min = 0, max = max(data$Remuneration, na.rm = TRUE), value = 75000)
               ),
               mainPanel(
                 plotOutput("selected_plot")
               )
             )
    )
  )
)

# Define server logic
server <- function(input, output) {
  
  agency_summary <- function(threshold) {
    # Filter data based on the threshold
    filtered_data <- subset(data, Remuneration > threshold)
    
    # Calculate total salary
    total_salary <- aggregate(Remuneration ~ Agency, data = filtered_data, FUN = sum, na.rm = TRUE)
    
    # Calculate total employees
    total_employees <- aggregate(Remuneration ~ Agency, data = filtered_data, FUN = length)
    
    # Merge total salary and total employees by Agency
    summarized_data <- merge(total_salary, total_employees, by = "Agency")
    
    # Calculate average salary
    summarized_data$AverageSalary <- summarized_data$Remuneration.x / summarized_data$Remuneration.y
    
    # Rename columns
    colnames(summarized_data) <- c("Agency", "TotalSalary", "TotalEmployees", "AverageSalary")
    
    return(summarized_data)
  }
  
  filtered_data <- reactive({
    filtered <- data
    
    # Sort table if checkbox is TRUE
    if(input$sort) {
      filtered <- filtered[order(filtered[, input$filter]), ]
    }
    
    # Filter by selected Sector
    if(input$filter != "All") {
      filtered <- filtered[filtered$Sector == input$filter, ]
    }
    
    return(filtered)
  })
  
  output$table <- DT::renderDataTable({
    DT::datatable(filtered_data(),
                  options = list(pageLength = 25))
  })
  
  output$result_count <- renderText({
    paste(nrow(filtered_data()), " results found")
  })
  
  output$downloadCSV <- downloadHandler(
    filename = function() {
      paste("filtered_data", ".csv", sep = "")
    },
    content = function(file) {
      filtered <- filtered_data()
      write.csv(filtered, file, row.names = FALSE)
    }
  )
  
  output$selected_plot <- renderPlot({
    req(input$plot_type)
    threshold <- input$salary_threshold
    
    if (input$plot_type == "Average Salary") {
      ggplot(data = agency_summary(threshold), aes(x = Agency, y = AverageSalary)) + 
        geom_bar(stat = "identity") + 
        labs(x = "Post-Secondary School", y = "Average Salary", 
             title = "Average Salary by School", 
             subtitle = paste("From employees that make above", scales::dollar(threshold))) +
        theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
        scale_y_continuous(labels = scales::dollar_format(prefix = "$"))
    } else if (input$plot_type == "Total Salary") {
      ggplot(data = agency_summary(threshold), aes(x = Agency, y = TotalSalary)) +
        geom_bar(stat = "identity") + 
        labs(x = "Post-Secondary School", y = "Total Salary", 
             title = "Total Salary by School", 
             subtitle = paste("From employees that make above", scales::dollar(threshold))) +
        theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
        scale_y_continuous(labels = scales::dollar_format(prefix = "$"))
    } else if (input$plot_type == "Total Employees") {
      ggplot(data = agency_summary(threshold), aes(x = Agency, y = TotalEmployees)) +
        geom_bar(stat = "identity") + 
        labs(x = "Post-Secondary School", y = "Total Employees",
             title = "Number of employees that make above certain salary") +
        theme(axis.text.x = element_text(angle = 45, hjust = 1))
    }
  })
}

# Run the application
shinyApp(
  ui = navbarPage(
    "Employee Salaries App",
    tabPanel("Home", ui_home),
    tabPanel("Data Analysis", ui_main)
  ),
  server = server
)

But now I want to have multiple pages. Each page would be a separate dataset. I saw in another post that using modules would be the best way to go about this. However, when I try that, I see the filter panels looks fine for the table and plot but the table and plots don't show up (which I assume means that it's something to do with the server command ?). My ui.R, server.R and module code is below.

ui.R

# Load necessary libraries
library(shiny)
library(shinydashboard)
library(DT)
library(ggplot2)
library(scales)



dashboardPage(
  
  dashboardHeader(
    title = "Public Sector Salaries Database"
  ),
  
  dashboardSidebar(
    sidebarMenu(id = "tabs",
                menuItem("Post-Secondary Schools", tabName = "module_uni"),
                menuItem("Healthcare Agencies", tabName = "module_health"),
                menuItem("Public School Districts", tabName = "module_schools")
    ),
    
    tags$hr(),
    
    uiOutput("globalenv"),
    
    div(style="padding: 20px;",
        helpText("app by ", a("Irvin Ng", href="www.linkedin.com/in/irvin-ng-605424120", target="_blank"))
    )
  ),
  
  dashboardBody(
    uiOutput("moduleUI")
  )
  
)

server.R

library(shiny)
library(shinydashboard)
library(DT)
library(ggplot2)
library(scales)

data <- read.csv("universities.csv", stringsAsFactors = FALSE)

function(input, output, session) {
  
  vals <- reactiveValues(
    selected_tab_new = NULL,
    selected_tab_old = NULL
  )
  
  observeEvent(input$tabs, {
    source(paste0("modules/", input$tabs, ".R"), local = FALSE)
    vals$selected_tab_new <- input$tabs
    
    if(!is.null(vals$selected_tab_old)) {
      rm(list = c(vals$selected_tab_old, paste0(vals$selected_tab_old, "UI")), pos = globalenv())
    }
    
    vals$selected_tab_old <- input$tabs
  })
  
  observe({
    if (!is.null(vals$selected_tab_new)) {
      output$moduleUI <- renderUI({
        rlang::as_function(paste0(vals$selected_tab_new, "UI"))(vals$selected_tab_new)
      })
      
      callModule(rlang::as_function(vals$selected_tab_new), id = vals$selected_tab_new)
    }
  })
  
}

module_uni.R


# Load necessary libraries
library(shiny)
library(DT)
library(ggplot2)
library(scales)

# Load the data
data <- read.csv("universities.csv", stringsAsFactors = FALSE)


# Define UI for the main app
module_uniUI <- function(id) {
  ns <- NS(id)
  fluidPage(
    titlePanel("Post-Secondary School Salaries"),
    
    tabsetPanel(
      tabPanel("Table", 
               sidebarLayout(
                 sidebarPanel(
                   checkboxInput("sort", "Sort table by selected variable"),
                   selectInput("filter", "Filter by Agency:",
                               choices = c("All", unique(data$Agency))),
                   downloadButton("downloadCSV", "Download CSV")
                 ),
                 mainPanel(
                   DT::dataTableOutput("table"),
                   textOutput("result_count")
                 )
               )
      ),
      tabPanel("Plots",
               sidebarLayout(
                 sidebarPanel(
                   h4("Select Plot"),
                   radioButtons("plot_type", "Choose a plot:",
                                choices = c("Average Salary", "Total Salary", "Total Employees")),
                   
                   # Adding a slider input for salary threshold
                   sliderInput("salary_threshold", "Select Salary Threshold:",
                               min = 0, max = max(data$Remuneration, na.rm = TRUE), value = 75000)
                 ),
                 mainPanel(
                   plotOutput("selected_plot")
                 )
               )
      )
    )
  )
}


module_uni <- function(input, output, data) {
  
  agency_summary <- function(threshold) {
    # Filter data based on the threshold
    filtered_data <- subset(data, Remuneration > threshold)
    
    # Calculate total salary
    total_salary <- aggregate(Remuneration ~ Agency, data = filtered_data, FUN = sum, na.rm = TRUE)
    
    # Calculate total employees
    total_employees <- aggregate(Remuneration ~ Agency, data = filtered_data, FUN = length)
    
    # Merge total salary and total employees by Agency
    summarized_data <- merge(total_salary, total_employees, by = "Agency")
    
    # Calculate average salary
    summarized_data$AverageSalary <- summarized_data$Remuneration.x / summarized_data$Remuneration.y
    
    # Rename columns
    colnames(summarized_data) <- c("Agency", "TotalSalary", "TotalEmployees", "AverageSalary")
    
    return(summarized_data)
  }
  
  filtered_data <- reactive({
    filtered <- data
    
    # Sort table if checkbox is TRUE
    if(input$sort) {
      filtered <- filtered[order(filtered[, input$filter]), ]
    }
    
    # Filter by selected Sector
    if(input$filter != "All") {
      filtered <- filtered[filtered$Sector == input$filter, ]
    }
    
    return(filtered)
  })
  
  output$table <- DT::renderDataTable({
    DT::datatable(filtered_data(),
                  options = list(pageLength = 25))
  })
  
  output$result_count <- renderText({
    paste(nrow(filtered_data()), " results found")
  })
  
  output$downloadCSV <- downloadHandler(
    filename = function() {
      paste("filtered_data", ".csv", sep = "")
    },
    content = function(file) {
      filtered <- filtered_data()
      write.csv(filtered, file, row.names = FALSE)
    }
  )
  
  output$selected_plot <- renderPlot({
    req(input$plot_type)
    threshold <- input$salary_threshold
    
    if (input$plot_type == "Average Salary") {
      ggplot(data = agency_summary(threshold), aes(x = Agency, y = AverageSalary)) + 
        geom_bar(stat = "identity") + 
        labs(x = "School", y = "Average Salary", 
             title = "Average Salary by Post-Secondary School", 
             subtitle = paste("From employees that make above", scales::dollar(threshold))) +
        theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
        scale_y_continuous(labels = scales::dollar_format(prefix = "$"))
    } else if (input$plot_type == "Total Salary") {
      ggplot(data = agency_summary(threshold), aes(x = Agency, y = TotalSalary)) +
        geom_bar(stat = "identity") + 
        labs(x = "School", y = "Total Salary", 
             title = "Total Salary by Post-Secondary School", 
             subtitle = paste("From employees that make above", scales::dollar(threshold))) +
        theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
        scale_y_continuous(labels = scales::dollar_format(prefix = "$"))
    } else if (input$plot_type == "Total Employees") {
      ggplot(data = agency_summary(threshold), aes(x = Agency, y = TotalEmployees)) +
        geom_bar(stat = "identity") + 
        labs(x = "School", y = "Total Employees",
             title = paste("Number of employees that make above", scales::dollar(threshold))) +
        theme(axis.text.x = element_text(angle = 45, hjust = 1))
    }
  })
}

There's no need to work out the switching of pages yourself, as the shinyDashboard package already supports it. I would put the UI for each of your tabs in the ui.R file, then include the observers unique to each together in server.R. This app doesn't look complex enough to warrant splitting it into several distinct files.

ui.R

# Load necessary libraries
library(shiny)
library(shinydashboard)
library(DT)
library(ggplot2)
library(scales)



dashboardPage(
  
  dashboardHeader(
    title = "Public Sector Salaries Database"
  ),
  
  dashboardSidebar(
    sidebarMenu(id = "tabs",
                menuItem("Post-Secondary Schools", tabName = "module_uni"),
                menuItem("Healthcare Agencies", tabName = "module_health"),
                menuItem("Public School Districts", tabName = "module_schools")
    ),
    
    tags$hr(),
    
    uiOutput("globalenv"),
    
    div(style="padding: 20px;",
        helpText("app by ", a("Irvin Ng", href="www.linkedin.com/in/irvin-ng-605424120", target="_blank"))
    )
  ),
  
  dashboardBody(
    tabItems(
         tabItem(
            tabName = "module_uni",
            #Tab UI goes here
         )
      )
)
)

Thank you so much for the reply. I tried to make a simple version using the observer as suggested on just one tab but the error still seems to persist (ie. The data tables and plots still aren't showing up). Here is my code:

# Load necessary libraries
library(shiny)
library(shinydashboard)
library(DT)
library(ggplot2)
library(scales)

# Load the data
uni <- read.csv("data/universities.csv", stringsAsFactors = FALSE)
health <- read.csv("data/health.csv", stringsAsFactors = FALSE)
schools <- read.csv("data/schools.csv", stringsAsFactors = FALSE)


ui_home <- fluidPage(
  titlePanel("Welcome to the BC Public Sector Employee Salaries App"),
  sidebarLayout(
    sidebarPanel(),
    mainPanel(
      h2("About"),
      p("This is a Shiny app that provides insights into the salaries of public sector employees in BC."),
      p("The vancouver Sun has collected the names and salaries of workers that earn a yearly salary of more than $75,000 in British Columbia."),
      p("Please navigate to the tabs above to explore the data."),
      img(src = "VancouverSun.png", width = "300px", height = "300px")
    )
  )
)

# Define UI for the main app
ui_main <- dashboardPage(
  dashboardHeader(),
  dashboardSidebar(
    sidebarMenu(id='BC Public Sectors',
                menuItem('Post-Secondary School Data', tabName='tab1')
    )
  ),
  dashboardBody(
    tabItems(
      tabItem('tab1',
              fluidPage(
                titlePanel("Post-Secondary School Data"),
                
                tabsetPanel(
                  tabPanel("Table", 
                           sidebarLayout(
                             sidebarPanel(
                               checkboxInput("sort", "Sort table by selected school"),
                               selectInput("filter", "Filter by school:",
                                           choices = c("All", unique(uni$Agency))),
                               downloadButton("downloadCSV", "Download CSV")
                             ),
                             mainPanel(
                               DT::dataTableOutput("table"),
                               textOutput("result_count")
                             )
                           )
                  ),
                  tabPanel("Plots",
                           sidebarLayout(
                             sidebarPanel(
                               h4("Select Plot"),
                               radioButtons("plot_type", "Choose a plot:",
                                            choices = c("Average Salary", "Total Salary", "Total Employees")),
                               
                               # Adding a slider input for salary threshold
                               sliderInput("salary_threshold", "Select Salary Threshold:",
                                           min = 0, max = max(uni$Remuneration, na.rm = TRUE), value = 75000)
                             ),
                             mainPanel(
                               plotOutput("selected_plot")
                             )
                           )
                  )
                )
              )
            )
    )
  )
)

# Define server logic
server <- function(input, output,session)
{
  
  observeEvent(input$tab1,
               {
                 agency_summary <- function(threshold) {
                   # Filter data based on the threshold
                   filtered_data <- subset(uni, Remuneration > threshold)
                   
                   # Calculate total salary
                   total_salary <- aggregate(Remuneration ~ Agency, data = filtered_data, FUN = sum, na.rm = TRUE)
                   
                   # Calculate total employees
                   total_employees <- aggregate(Remuneration ~ Agency, data = filtered_data, FUN = length)
                   
                   # Merge total salary and total employees by Agency
                   summarized_data <- merge(total_salary, total_employees, by = "Agency")
                   
                   # Calculate average salary
                   summarized_data$AverageSalary <- summarized_data$Remuneration.x / summarized_data$Remuneration.y
                   
                   # Rename columns
                   colnames(summarized_data) <- c("Agency", "TotalSalary", "TotalEmployees", "AverageSalary")
                   
                   return(summarized_data)
                 }
                 
                 filtered_data <- reactive({
                   filtered <- uni
                   
                   # Sort table if checkbox is TRUE
                   if(input$sort) {
                     filtered <- filtered[order(filtered[, input$filter]), ]
                   }
                   
                   # Filter by selected Sector
                   if(input$filter != "All") {
                     filtered <- filtered[filtered$Sector == input$filter, ]
                   }
                   
                   return(filtered)
                 })
                 
                 output$table <- DT::renderDataTable({
                   DT::datatable(filtered_data(),
                                 options = list(pageLength = 25))
                 })
                 
                 output$result_count <- renderText({
                   paste(nrow(filtered_data()), " results found")
                 })
                 
                 output$downloadCSV <- downloadHandler(
                   filename = function() {
                     paste("filtered_data", ".csv", sep = "")
                   },
                   content = function(file) {
                     filtered <- filtered_data()
                     write.csv(filtered, file, row.names = FALSE)
                   }
                 )
                 
                 output$selected_plot <- renderPlot({
                   req(input$plot_type)
                   threshold <- input$salary_threshold
                   
                   if (input$plot_type == "Average Salary") {
                     ggplot(data = agency_summary(threshold), aes(x = Agency, y = AverageSalary)) + 
                       geom_bar(stat = "identity") + 
                       labs(x = "Post-Secondary School", y = "Average Salary", 
                            title = "Average Salary by School", 
                            subtitle = paste("From employees that make above", scales::dollar(threshold))) +
                       theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
                       scale_y_continuous(labels = scales::dollar_format(prefix = "$"))
                   } else if (input$plot_type == "Total Salary") {
                     ggplot(data = agency_summary(threshold), aes(x = Agency, y = TotalSalary)) +
                       geom_bar(stat = "identity") + 
                       labs(x = "Post-Secondary School", y = "Total Salary", 
                            title = "Total Salary by School", 
                            subtitle = paste("From employees that make above", scales::dollar(threshold))) +
                       theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
                       scale_y_continuous(labels = scales::dollar_format(prefix = "$"))
                   } else if (input$plot_type == "Total Employees") {
                     ggplot(data = agency_summary(threshold), aes(x = Agency, y = TotalEmployees)) +
                       geom_bar(stat = "identity") + 
                       labs(x = "Post-Secondary School", y = "Total Employees",
                            title = "Number of employees that make above certain salary") +
                       theme(axis.text.x = element_text(angle = 45, hjust = 1))
                   }
                 })
               })
  
}

# Run the application
shinyApp(
  ui = navbarPage(
    "Employee Salaries App",
    tabPanel("Home", ui_home),
    tabPanel("Salary Data", ui_main)
  ),
  server = server
)

Maybe a namespace issue:

See the use of NS and ns:

Shiny - Modularizing Shiny app code (posit.co)

Chapter 19 Shiny modules | Mastering Shiny (mastering-shiny.org)