library(shiny)
library(shiny.semantic)
library(tidyverse)
library(readr)
How to Build Accordions in R Shiny Using Shiny.Semantic
The accordion is an underrated data visualization. It can display text, tables, and small graphs in a confined space, which makes it easy for the user to quickly read through key data points.
At Free State Analytics, we recently built the following accordions for an application. We planned to use these accordions in a few places throughout the application, so we made extensive use of modules to make this functionality possible.
This tutorial will teach you how to build this custom accordion using R Shiny and Appsilon’s R Shiny Semantic. We will not use a pre-built function that produces an accordion, but rather reference the semantic CSS directly using the div
function.
The tutorial will use two pre-cleaned data sets for household carbon emissions to report in the accordion. The accordion requirements include:
- Aggregated emissions reported at the top
- Aggregated emissions reported by category
- Aggregated emissions reported by sub-category
- Dynamic time period labels for the accordion title, total metric at the top, and each of the headings
- The ability to expand or collapse the sub-category figures under the category headings
- The ability to re-use the module for various time periods
For those who find it easier to learn through reverse engineering other people’s code, you can find the complete code for the modules here.
Otherwise, keep reading for a step-by-step tutorial.
Prerequisites
This tutorial requires the following packages to work:
You’ll also want to download this data set here:
<- read_csv("https://raw.githubusercontent.com/taylorrodgers/public_datasets/main/household_carbon_estimates.csv")
household_estimates <- read_csv("https://raw.githubusercontent.com/taylorrodgers/public_datasets/main/total_household_estimates.csv") total_estimates
When it comes to prior knowledge, you should know the following:
The purpose of a Shiny module, at least conceptually speaking
How to write and use custom R functions
However, it’s not important to be an expert in these things. This tutorial will improve your skills in these areas with this real-world example.
Building Shiny App Foundation
Since our accordion will use Shiny Semantic, we’ll use the semanticPage
function in our application’s UI.
Down below are the ui
and server
that will later call our modules.
library(shiny)
library(shiny.semantic)
library(tidyverse)
library(readr)
<- read_csv("https://raw.githubusercontent.com/taylorrodgers/public_datasets/main/household_carbon_estimates.csv")
household_estimates <- read_csv("https://raw.githubusercontent.com/taylorrodgers/public_datasets/main/total_household_estimates.csv")
total_estimates
<- function() {
ui semanticPage(
h2("Our Accordions"),
div(class="ui stackable grid",
div(class="four wide column",
h3("Totals by Category"),
div(class="ui raised segment",
style="min-width: 550px;",
tableOutput("TestTable")
)
)
)
)
}
<- function(input, output, session) {
server
$TestTable <- renderTable({
output
total_estimates
})
}
shinyApp(ui = ui(), server = server)
Right now, our app is fairly empty. We’ll add more and more to it as the tutorial progresses. I included TestTable
to allow you to validate your work as you go.
One thing to note is how I use the div
functions and the class
argument within them.
The class
argument allows us to reference specific CSS elements defined in the Fomantic-UI framework. Fomantic-UI is the new name for Semantic-UI, which shiny.semantic
uses.
You can use this link for a reference on how these elements are used: https://fomantic-ui.com/
We’ll make heavy use of the div
function and its class
argument throughout the tutorial.
Creating “Master” Accordion Modules
Let’s build the module to house our accordion. We’ll need to use a Shiny module because we’ll re-use the same accordion for reporting a household’s carbon emissions in three separate time periods (week, month, and year).
Modules always come in pairs: a ui and a server. We’ll call these modules accordionMasterUI
and accordionMasterServer
, respectively.
<- function(id) {
accordionMasterUI
tagList(
div(class = "ui segment",
h3("Yearly Carbon Estimates"),
h3(class="ui grey sub header","Total CO2 Emissions"),
h2("KPI Placeholder"),
br()
)
)
}
<- function(id) {
accordionMasterServer moduleServer(id,function(input,output,session) {
}) }
We use “ui segment” for our class
argument in the div
function above. This will make a nice clean border around our future accordion. You can read about segments here: https://fomantic-ui.com/elements/segment.html
We use h2
and h3
to determine how our labels appear. Just like with div
, you can use the class
argument to determine the way the header will display. You can read more about headers here: https://fomantic-ui.com/elements/header.html
Let’s add this module to our app’s ui
and server
functions.
<- function() {
ui semanticPage(
h2("Our Accordions"),
div(class="ui stackable grid",
div(class="three wide column",
style="min-width: 350px;",
accordionMasterUI("accordion")
),div(class="four wide column",
h3("Totals by Category"),
div(class="ui raised segment",
style="min-width: 550px;",
tableOutput("TestTable")
)
)
)
)
}
<- function(input, output, session) {
server
accordionMasterServer("accordion")
$TestTable <- renderTable({
output
total_estimates
})
}
shinyApp(ui = ui(), server = server)
This app still doesn’t reveal much information, but you can at least see the look of our accordionMasterUI
.
Let’s start building the accordion itself.
Building Our Accordion
The semantic CSS element for an accordion is “ui accordion.” So we’ll need to create a new div
and set class="ui accordion"
.
Let’s add the accordion itself to accordionMasterUI
function we just created, like so:
<- function(id) {
accordionMasterUI tagList(
div(class = "ui segment",
h3("Yearly Carbon Estimates"),
h3(class="ui grey sub header","Total CO2 Emissions"),
h2("KPI Placeholder"),
br(),
div(class="ui accordion"
)
)
) }
Within the new accordion, we’ll need additional div
functions. These will come in pairs for each section of the accordion: title and content. We’ll use the class
argument to specify them.
Down below, I added two pairs of div
functions. These also include the headers for the title and a placeholder value for the content.
<- function(id) {
accordionMasterUI tagList(
div(class = "ui segment",
h3("Yearly Carbon Estimates"),
h3(class="ui grey sub header","Total CO2 Emissions"),
h2("KPI Placeholder"),
br(),
div(class="ui accordion",
### Title
div(class ="title",
h4(class="ui dividing header","Travel")
),### Content
div(class = "content",
p("Content placeholder")
),### Title
div(class ="title",
h4(class="ui dividing header","Housing")
),### Content
div(class = "content",
p("Content placeholder")
)
)
)
) }
If you run the app, the accordion won’t currently expand or collapse the content. We’ll need to write a few more lines of code before this functionality works.
First, we can define which sections of the accordion will automatically expand whenever the user opens the application. This will give the user a hint that the accordion is interactive. Otherwise, they may assume the accordion is a simple list of static KPI metrics.
Down below, I update the class
argument for the first pair of div
functions to “title active” and “content active”.
<- function(id) {
accordionMasterUI tagList(
div(class = "ui segment",
h3("Yearly Carbon Estimates"),
h3(class="ui grey sub header","Total CO2 Emissions"),
h2("KPI Placeholder"),
br(),
div(class="ui accordion",
### Title
div(class ="title active",
h4(class="ui dividing header","Travel")
),### Content
div(class = "content active",
p("Content placeholder")
),### Title
div(class ="title",
h4(class="ui dividing header","Housing")
),### Content
div(class = "content",
p("Content placeholder")
)
)
)
) }
Now when you run the app, you’ll see the “content placeholder” appear immediately.
However, we still can’t click and expand the housing content. We’ll need to add a JavaScript function to enable that interactivity.
Adding the JavaScript Function
At the top of your application script, right after you load your packages, add the following:
# Javascript for Accordion
<- "
accordion_js $(document).ready(function() {
$('.ui.accordion').accordion();
})"
Inside the accordionMasterUI
function, add shiny::tags$script(accordion_js)
to the tagList
, like so:
<- function(id) {
accordionMasterUI tagList(
::tags$script(accordion_js),
shinydiv(class = "ui segment",
...
)
) }
Run your app again. Click around on the accordion and you’ll notice the sections expand and collapse.
accordion
Function?
Within the shiny.semantic
package, there is a function called accordion
that enables a similar feature. I’m personally not a fan of using functions to call CSS elements.
A key benefit that R Shiny has over other dashboard dev tools is the precision. We can define exactly the functionality we want when we write out the scripts ourselves.
While the shiny.semantic
package is great for writing reactive programming with the semantic CSS framework, the functions unrelated to inputs or outputs don’t always provide the precision we need.
In those instances, I find it better to simply use div(class = [element name], …)
to define the interface features.
Adding Real Data to the Accordion
Let’s add real data to our accordion.
First, let’s replace the “KPI placeholder” in our h2
with a textOutput
to show the total carbon emissions across categories.
<- function(id) {
accordionMasterUI tagList(
div(class = "ui segment",
h3("Yearly Carbon Estimates"),
h3(class="ui grey sub header","Total CO2 Emissions"),
h2(textOutput(NS(id,"GrandTotal"))),
...
)
) }
We’ll need to pair this textOutput
function with renderText
in our accordionMasterServer
.
<- function(id) {
accordionMasterServer moduleServer(id,function(input,output,session) {
$GrandTotal <- renderText({
output%>%
total_estimates filter(total_type=="Grand") %>%
transmute(grandtotal=round(yearly_emissions,2)) %>%
paste0(.)
})
})
}
Now when you run the application, you’ll see a grand total for household emissions at the top of the accordion.
Namespaces are a key concept with modules. In plain english, it reduces conflicts when using the same module repeatedly within an application.
For example, in the ui
function above, we have a textOutput
named “GrandTotal.” That is paired with output$GrandTotal <- renderText({…})
in the server
object.
If a Shiny app re-uses this module (which ours will), we’ll have multiple “GrandTotal” outputs in our session. The app won’t know how to pair the outputs effectively between the ui and the server.
Whenever we use NS(id, "GrandTotal")
to assign the textOutput
an id, we are prefixing the name “GrandTotal” with the id we’ll provide the module.
In our final script for this tutorial, we’ll call our accordionMasterUI
module three times and name them year, month, and week. That will turn the three ambigiously named “GrandTotal” outputs into “year.GrandTotal”, “month.GrandTotal”, and “week.GrandTotal”, respectively, in our session.
That eliminates the conflict for the app and allows all three to run simultaneously.
Let’s also replace the placeholders for our titles and contents with either textOutput
or tableOutput
, respectively:
<- function(id) {
accordionMasterUI tagList(
::tags$script(accordion_js),
shinydiv(class = "ui segment",
h3("Yearly Carbon Estimates"),
h3(class="ui grey sub header","Total CO2 Emissions"),
h2(textOutput(NS(id,"GrandTotal"))),
br(),
div(class="ui accordion",
### Title
div(class ="title active",
h4(class="ui dividing header",textOutput(NS(id,"TravelTotal")))
),### Content
div(class ="content active",
tableOutput(NS(id,"TravelSubTotals"))
),### Title
div(class ="title",
h4(class="ui dividing header",textOutput(NS(id,"HousingTotal")))
),### Content
div(class ="content",
tableOutput(NS(id,"HousingSubTotals"))
)
)
)
) }
We’ll also need to update the server module function to present these outputs, like so:
<- function(id) {
accordionMasterServer moduleServer(id,function(input,output,session) {
$GrandTotal <- renderText({
output%>%
total_estimates transmute(grandtotal=round(yearly_emissions),2)) %>%
paste0(.)
})
$TravelTotal <- renderText({
output%>%
total_estimates filter(total_type=="Travel") %>%
transmute(emissions=round(yearly_emissions,2)) %>%
paste0(.," /yr")
})
$TravelSubTotals <- renderTable({
output%>%
household_estimates filter(categories=="Travel") %>%
arrange(subcategories) %>%
group_by(subcategories) %>%
summarize(emissions=sum(round(yearly_emissions,2))) %>%
rename(Subcategories=subcategories,Emissions=emissions)
})
$HousingTotal <- renderText({
output%>%
total_estimates filter(total_type=="Housing") %>%
transmute(emissions=round(yearly_emissions,2)) %>%
paste0(.," /yr")
})
$HousingSubTotals <- renderTable({
output%>%
household_estimates filter(categories=="Housing") %>%
arrange(subcategories) %>%
group_by(subcategories) %>%
summarize(emissions=sum(round(yearly_emissions,2))) %>%
rename(Subcategories=subcategories,Emissions=emissions)
})
}) }
Run the app again and click around on the accordion. Something isn’t working, right?
The accordion won’t reveal the data for the housing content section.
We’ll need to add the following script to our accordionMasterServer
module function. Go ahead and run the app afterwards to see how it responds.
outputOptions(output, "HousingSubTotals", suspendWhenHidden = FALSE)
I had learned about how the function above fixes this problem in a stack overflow comment. Sadly, I did not save the link. I wish I could give proper credit.
Adding Dynamic Date Selectors and Labels
A key requirement for this accordion module relates to dates. We want to use the same module three times to report on carbon emissions at the year, month, and week level. So far, we’ve hard coded the time period to be a year.
If we want the module to report on month or week instead, we will need the module select the proper column from the data sets and update the labels within the accordion.
We can drive this change throughout the module by adding “period” as an argument to our accordionMasterUI
and accordionMasterServer
module functions, like so:
<- function(id,period) {
accordionMasterUI
...
}
<- function(id,period) {
accordionMasterServer
... }
We also need to update the ui
and server
functions for our application, which is where we call the accordion modules.
Down below, I added the period="year"
to both accordionMasterUI
and accordionMasterServer
.
<- function() {
ui semanticPage(
h2("Our Accordions"),
div(class="ui stackable grid",
div(class="three wide column",
style="min-width: 350px;",
accordionMasterUI("accordion",period="year")
),div(class="four wide column",
h3("Totals by Category"),
div(class="ui raised segment",
style="min-width: 550px;",
tableOutput("TestTable")
)
)
)
)
}
<- function(input, output, session) {
server
accordionMasterServer("accordion",period="year")
$TestTable <- renderTable({
output
total_estimates
})
}
This doesn’t change anything in our accordion, yet. We still need to apply these new arguments in our accordion modules.
Let’s update our data wrangling functions to select the appropriate columns. We can use the select
, contains
, and rename
functions from the tidyverse
package to make this easy.
Down below is what the data wrangling scripts would look outside of an application. Notice that I define the period as “year” and use select
to grab any column that contains the word “year.” We could do the same for month and week as well.
<- "year"
period
%>%
total_estimates select(total_type,contains(period)) %>%
rename(emissions=2) %>%
filter(total_type=="Travel") %>%
transmute(emissions=round(emissions,2)) %>%
paste0(.," /yr")
%>%
household_estimates select(categories,subcategories,contains(period)) %>%
rename(emissions=3) %>%
filter(categories=="Travel") %>%
arrange(subcategories) %>%
group_by(subcategories) %>%
summarize(emissions=sum(round(emissions,2))) %>%
rename(Subcategories=subcategories,Emissions=emissions)
One thing that we’ll need to do in the app is attach the proper label to the category totals reported. So “/yr”, “/mo”, or “/wk”.
We can write an ifelse
statement to accomplish this, like so:
<- "year"
period
%>%
total_estimates select(total_type,contains(period)) %>%
rename(emissions=2) %>%
filter(total_type=="Travel") %>%
transmute(emissions=round(emissions,2)) %>%
paste0(.,ifelse(period=="year"," /yr",
ifelse(period=="month"," /mo",
" /wk")))
Let’s add this to our accordionMasterServer
module function:
<- function(id,period) {
accordionMasterServer moduleServer(id,function(input,output,session) {
$GrandTotal <- renderText({
output%>%
total_estimates transmute(grandtotal=round(yearly_emissions),2)) %>%
paste0(.)
})
$TravelTotal <- renderText({
output%>%
total_estimates select(total_type,contains(period)) %>%
rename(emissions=2) %>%
filter(total_type=="Travel") %>%
transmute(emissions=round(emissions,2)) %>%
paste0(.,ifelse(period=="year"," /yr",
ifelse(period=="month"," /mo"," /wk")))
})
$TravelSubTotals <- renderTable({
output%>%
household_estimates select(categories,subcategories,contains(period)) %>%
rename(emissions=3) %>%
filter(categories=="Travel") %>%
arrange(subcategories) %>%
group_by(subcategories) %>%
summarize(emissions=sum(round(emissions,2))) %>%
rename(Subcategories=subcategories,Emissions=emissions)
})
$HousingTotal <- renderText({
output%>%
total_estimates select(total_type,contains(period)) %>%
rename(emissions=2) %>%
filter(total_type=="Housing") %>%
transmute(emissions=round(emissions,2)) %>%
paste0(.,ifelse(period=="year"," /yr",
ifelse(period=="month"," /mo"," /wk")))
})
$HousingSubTotals <- renderTable({
output%>%
household_estimates select(categories,subcategories,contains(period)) %>%
rename(emissions=3) %>%
filter(categories=="Housing") %>%
arrange(subcategories) %>%
group_by(subcategories) %>%
summarize(emissions=sum(round(emissions,2))) %>%
rename(Subcategories=subcategories,Emissions=emissions)
})
outputOptions(output, "HousingSubTotals", suspendWhenHidden = FALSE)
}) }
You can change the period
argument to “month” or “week” in the app and see how it changes for you. You can use the “TestTable” on the right of the app to verify the results.
<- function() {
ui semanticPage(
h2("Our Accordions"),
div(class="ui stackable grid",
div(class="three wide column",
style="min-width: 350px;",
accordionMasterUI("accordion",period="month")
),div(class="four wide column",
h3("Totals by Category"),
div(class="ui raised segment",
style="min-width: 550px;",
tableOutput("TestTable")
)
)
)
)
}
<- function(input, output, session) {
server
accordionMasterServer("accordion",period="month")
$TestTable <- renderTable({
output
total_estimates
})
}
shinyApp(ui = ui(), server = server)
Our accordion isn’t fully dynamic though. We still have a header at the top showing “Yearly Carbon Estimates,” even when we set the period
argument to week. The same for our grand total KPI at the top, which is still hard coded to show the yearly data and still doesn’t have a label itself.
We can fix the label at the top real easy with a another ifelse
function.
<- function(id,period) {
accordionMasterUI tagList(
::tags$script(accordion_js),
shinydiv(class = "ui segment",
h3(paste(ifelse(period=="year","Yearly",
ifelse(period=="month","Monthly","Weekly")),
"Carbon Estimates")),
...
)
) }
Our grand total output needs a label too and also needs to report the proper data, based on the period
argument.
The label for the grand total is trickier to pull off than the others.
Down below is what we’re working towards with this tutorial. If you pay close attention, the actual metric and the ” /month” label have some space between them. That’s because they have two separate formats. The time period label is smaller than the metric. We’ll need to use two textOutput
functions to make this work.
In the accordionMasterUI
, let’s create a new set of div
functions for our grand total output and label. We’ll use the “ui grid” element, a “four column row”, and two “columns” within it, like so:
<- function(id,period) {
accordionMasterUI tagList(
::tags$script(accordion_js),
shinydiv(class = "ui segment",
h3(paste(ifelse(period=="year","Yearly",
ifelse(period=="month","Monthly","Weekly")),
"Carbon Estimates")),
h3(class="ui grey sub header","Total CO2 Emissions"),
div(class="ui grid",
div(class="four column row",
div(class="column",
h2(textOutput(NS(id,"GrandTotal")))),
div(class="column",
" /year"
)
)),
…
)
) }
Run the app again and see how it looks.
Doesn’t look bad, does it? Still needs improvement.
If we set the class
argument for the h2
to “ui dividing header”, it’ll give the “GrandTotal” output a nice little underline. We can also wrap the ” /year” text in strong
to make it bolder.
<- function(id,period) {
accordionMasterUI tagList(
::tags$script(accordion_js),
shinydiv(class = "ui segment",
h3(paste(ifelse(period=="year","Yearly",
ifelse(period=="month","Monthly","Weekly")),
"Carbon Estimates")),
div(class="ui grid",
div(class="four column row",
div(class="column",
h2(class="ui dividing header",
textOutput(NS(id,"GrandTotal")))),
div(class="column",
strong(" /year")
)
)),
…
)
) }
Now it looks a lot more polished than before.
The period label still isn’t dynamic though. Let’s replace ” /year” with the script below so that it changes with the period the module uses.
ifelse(period=="year"," /year",
ifelse(period=="month"," /month"," /week"))
We’re almost done with our grand total KPI. One last thing to do is update the output itself to select the proper column, depending on the period
argument of the modules.
In accordionMasterServer
, update output$GrandTotal
to select the proper column, like so:
$GrandTotal <- renderText({
output%>%
total_estimates select(total_type,contains(period)) %>%
rename(emissions=2) %>%
filter(total_type=="Grand") %>%
transmute(grandtotal=round(emissions,2)) %>%
paste0(.)
})
Run the app again and see how it looks. Starting to come together, right?
We still need to include other categories in our accordion. However, we’re about to reach a particular threshold with our code where it’s better to reduce redundancies before moving forward. This is an ideal point to create an additional set of modules and functions. This will provide more ease with improving the aesthetics of our accordion’s titles and content.
Checkpoint
We’re about to change a lot of things. In order to make sure we’re on the same page, check your code against the code below, or simply copy and paste the entire script.
library(shiny)
library(shiny.semantic)
library(tidyverse)
library(readr)
<- "
accordion_js $(document).ready(function() {
$('.ui.accordion').accordion();
})"
<- read_csv("https://raw.githubusercontent.com/taylorrodgers/public_datasets/main/household_carbon_estimates.csv")
household_estimates <- read_csv("https://raw.githubusercontent.com/taylorrodgers/public_datasets/main/total_household_estimates.csv")
total_estimates
<- function(id,period) {
accordionMasterUI tagList(
::tags$script(accordion_js),
shinydiv(class = "ui segment",
h3(paste(ifelse(period=="year","Yearly",
ifelse(period=="month","Monthly","Weekly")),
"Carbon Estimates")),
h3(class="ui grey sub header","Total CO2 Emissions"),
div(class="ui grid",
div(class="four column row",
div(class="column",
h2(class="ui dividing header",textOutput(NS(id,"GrandTotal")))),
div(class="column",
strong(
ifelse(period=="year"," /year",
ifelse(period=="month"," /month"," /week"))
)
)
)),br(),
div(class="ui accordion",
### Title
div(class ="title active",
h4(class="ui dividing header",textOutput(NS(id,"TravelTotal")))
),### Content
div(class ="content active",
tableOutput(NS(id,"TravelSubTotals"))
),### Title
div(class ="title",
h4(class="ui dividing header",textOutput(NS(id,"HousingTotal")))
),### Content
div(class ="content",
tableOutput(NS(id,"HousingSubTotals"))
)
)
)
)
}
<- function(id,period) {
accordionMasterServer moduleServer(id,function(input,output,session) {
$GrandTotal <- renderText({
output%>%
total_estimates select(total_type,contains(period)) %>%
rename(emissions=2) %>%
filter(total_type=="Grand") %>%
transmute(grandtotal=round(emissions,2)) %>%
paste0(.)
})
$TravelTotal <- renderText({
output%>%
total_estimates select(total_type,contains(period)) %>%
rename(emissions=2) %>%
filter(total_type=="Travel") %>%
transmute(emissions=round(emissions,2)) %>%
paste0(.,ifelse(period=="year"," /yr",
ifelse(period=="month"," /mo"," /wk")))
})
$TravelSubTotals <- renderTable({
output%>%
household_estimates select(categories,subcategories,contains(period)) %>%
rename(emissions=3) %>%
filter(categories=="Travel") %>%
arrange(subcategories) %>%
group_by(subcategories) %>%
summarize(emissions=sum(round(emissions,2))) %>%
rename(Subcategories=subcategories,Emissions=emissions)
})
$HousingTotal <- renderText({
output%>%
total_estimates select(total_type,contains(period)) %>%
rename(emissions=2) %>%
filter(total_type=="Housing") %>%
transmute(emissions=round(emissions,2)) %>%
paste0(.,ifelse(period=="year"," /yr",
ifelse(period=="month"," /mo"," /wk")))
})
$HousingSubTotals <- renderTable({
output%>%
household_estimates select(categories,subcategories,contains(period)) %>%
rename(emissions=3) %>%
filter(categories=="Housing") %>%
arrange(subcategories) %>%
group_by(subcategories) %>%
summarize(emissions=sum(round(emissions,2))) %>%
rename(Subcategories=subcategories,Emissions=emissions)
})
outputOptions(output, "HousingSubTotals", suspendWhenHidden = FALSE)
})
}
<- function() {
ui semanticPage(
h2("Our Accordions"),
div(class="ui stackable grid",
div(class="three wide column",
style="min-width: 350px;",
accordionMasterUI("accordion",period="month")
),div(class="four wide column",
h3("Totals by Category"),
div(class="ui raised segment",
style="min-width: 550px;",
tableOutput("TestTable")
)
)
)
)
}
<- function(input, output, session) {
server
accordionMasterServer("accordion",period="month")
$TestTable <- renderTable({
output
total_estimates
})
}
shinyApp(ui = ui(), server = server)
Reducing Code Redundancies
As you may have noticed, our accordionMasterServer
module function has redundant and repetitive code. In shorter words, we have the same scripts doing the same things with one minor change, such as the “categories” filter.
This isn’t too big of a deal on its own, but let’s say we intend to re-use our accordion module across multiple apps. It becomes harder to make minor changes while maintaining consistency without updating many lines of code.
We can reduce these redundancies using both functions and modules.
Let’s start with the low hanging fruit, which is our data wrangling scripts.
Building a Function for Our Total and Subtotals
Right now, we calculate the total emissions for the travel and housing categories separately, as well as a small table presenting emissions by subcategories.
We have five categories overall, which means we would have ten scripts that perform the same steps, unless we turn them into a function.
Down below is the script to calculate total estimates:
%>%
total_estimates select(total_type,contains(period)) %>%
rename(emissions=2) %>%
filter(total_type=="Travel") %>%
transmute(emissions=round(emissions,2)) %>%
paste0(.,ifelse(period=="year"," /yr",
ifelse(period=="month"," /mo"," /wk")))
Let’s put this in a function called “Totals.” This function will only need the category and time period passed through as arguments, since those are the only variations in the way we use this script currently.
<- function(category,period) {
Totals
%>%
total_estimates select(total_type,contains(period)) %>%
rename(emissions=2) %>%
filter(total_type==category) %>%
transmute(emissions=round(emissions,2)) %>%
paste0(.,ifelse(period=="year"," /yr",
ifelse(period=="month"," /mo"," /wk")))
}
We can do the same thing for the subtotals. Down below is what it looked like for the travel category and its replacement in functional form.
%>%
household_estimates select(categories,subcategories,contains(period)) %>%
rename(emissions=3) %>%
filter(categories=="Travel") %>%
arrange(subcategories) %>%
group_by(subcategories) %>%
summarize(emissions=sum(round(emissions,2))) %>%
rename(Subcategories=subcategories,Emissions=emissions)
<- function(category,period) {
SubTotals
%>%
household_estimates select(categories,subcategories,contains(period)) %>%
rename(emissions=3) %>%
filter(categories==category) %>%
arrange(subcategories) %>%
group_by(subcategories) %>%
summarize(emissions=sum(round(emissions,2))) %>%
rename(Subcategories=subcategories,Emissions=emissions)
}
Now when we want to run this script, all we have to do is enter the function name and the category.
Totals("Travel","year")
SubTotals("Travel","year")
Add both the Totals
and SubTotals
functions to the top of your app script. You can then call those functions within the accordionMasterServer
function, like so:
<- function(id,period) {
accordionMasterServer moduleServer(id,function(input,output,session) {
$GrandTotal <- renderText({
output%>%
total_estimates select(total_type,contains(period)) %>%
rename(emissions=2) %>%
filter(total_type=="Grand") %>%
transmute(grandtotal=round(emissions,2)) %>%
paste0(.)
})
$TravelTotal <- renderText({
outputTotals("Travel",period)
})
$TravelSubTotals <- renderTable({
outputSubTotals("Travel",period)
})
$HousingTotal <- renderText({
outputTotals("Housing",period)
})
$HousingSubTotals <- renderTable({
outputSubTotals("Housing",period)
})
outputOptions(output, "HousingSubTotals", suspendWhenHidden = FALSE)
}) }
Our script already looks shorter, doesn’t it?
We can take it even further though.
As you may have noticed, output$TravelTotal
is nearly identical to output$HousingTotal
. Same with output$TravelSubTotals
and output$HousingSubTotals
. The only difference is the category.
However, since these outputs are paired with functions within accordionMasterUI
, we’ll need to create additional modules to reduce these redundancies further.
Building Modules for Titles and Contents
In our accordion, we have a pair of div
functions: one with a class
of “title” and the other “content”.
The outputs within these div
functions have to be matched on the server side. It’s not a burden to maintain these right now, but as we add more categories, it’ll be harder to tinker and improve the aesthetics with such a long script.
We can modularlize the div
function pairs on the ui-side and the outputs on the server-side to make life easier for us.
Let’s start with the ui
module.
Down below is the current script we use for each accordion section, with the exception of the namespace ID, which is the only thing that changes.
### Title
div(class ="title active",
h4(class="ui dividing header",textOutput(NS(id,"TravelTotal")))
),### Content
div(class ="content active",
tableOutput(NS(id,"TravelSubTotals"))
)
We can wrap this script in a tagList()
and place it inside our new accordionTitleContentUI
module function. We simply need to remove the word “Travel” from the namespaces.
<- function (id) {
accordionTitleContentUI tagList(
div(class = "title active",
h4(class="ui dividing header",
textOutput(NS(id,"Total")))
),div(class = "content active",
tableOutput(NS(id,"SubTotal"))
)
) }
Now, there is an issue with the module above that you may have noticed. It would set all sections in the accordion to “active,” which means each section would be expanded when you open the application.
We can add an indicator argument to tell the module to make the class
“active” only when we want.
In the script below, I update the module function with this argument and an ifelse
.
<- function (id,active_ind=FALSE) {
accordionTitleContentUI tagList(
div(class = ifelse(active_ind==TRUE,"title active","title"),
h4(class="ui dividing header",textOutput(NS(id,"Total")))
),div(class = ifelse(active_ind==TRUE,"content active","content"),
tableOutput(NS(id,"SubTotal"))
)
) }
This module won’t display data on its own. We need to pair it with a server module function.
Down below, I take what used to be individual outputs in accordionMasterServer
and place them in the new module function.
<- function(id,category,period) {
accordionTitleContentServer moduleServer(id, function(input,output,session) {
$Total <- renderText({
outputpaste0("Total ",category,": ",Totals(category=category,period))
})
$SubTotal <- renderTable({
outputSubTotals(category,period)
})
outputOptions(output, "SubTotal", suspendWhenHidden = FALSE)
}) }
As you may have noticed, I replaced the hard coded filters of “Travel” or “Housing” with the argument category
. I also added a dynamic label to output$Total
that pastes the category
argument with the metric reported.
Add these two module functions after your Totals
and SubTotals
function, but before your accordionMasterUI
function, in your app script.
Afterwards, you can replace the redundant code accordionMasterUI
and accordionMasterServer
with these new modules, like so:
<- function(id,period) {
accordionMasterUI tagList(
::tags$script(accordion_js),
shinydiv(class = "ui segment",
h3(paste(ifelse(period=="year","Yearly",
ifelse(period=="month","Monthly","Weekly")),
"Carbon Estimates")),
h3(class="ui grey sub header","Total CO2 Emissions"),
div(class="ui grid",
div(class="four column row",
div(class="column",
h2(class="ui dividing header",textOutput(NS(id,"GrandTotal")))),
div(class="column",
strong(
ifelse(period=="year"," /year",
ifelse(period=="month"," /month"," /week"))
)
)
)),br(),
div(class="ui accordion",
accordionTitleContentUI(NS(id,"Travel"),active_ind=TRUE),
accordionTitleContentUI(NS(id,"Housing"),active_ind=FALSE)
)
)
)
}
<- function(id,period) {
accordionMasterServer moduleServer(id,function(input,output,session) {
$GrandTotal <- renderText({
output%>%
total_estimates select(total_type,contains(period)) %>%
rename(emissions=2) %>%
filter(total_type=="Grand") %>%
transmute(grandtotal=round(emissions,2)) %>%
paste0(.)
})
accordionTitleContentServer("Travel",category="Travel",period)
accordionTitleContentServer("Housing",category="Housing",period)
}) }
If you run the app now, it won’t look much different than before (with the exception of the category labels). We still have some aesthetic improvement to do.
However, these modules will make it a lot easier to make the tweaks in the next few sections. Rather than go through several lines of redundant code, we only have to make the change once before testing it.
Checkpoint
Down below is the code we’ve written so far. If anything does not look right, try comparing your code to mine and see where the mistake might be:
library(shiny)
library(shiny.semantic)
library(tidyverse)
library(readr)
<- read_csv("https://raw.githubusercontent.com/taylorrodgers/public_datasets/main/household_carbon_estimates.csv")
household_estimates <- read_csv("https://raw.githubusercontent.com/taylorrodgers/public_datasets/main/total_household_estimates.csv")
total_estimates
# Javascript for Accordion
<- "
accordion_js $(document).ready(function() {
$('.ui.accordion').accordion();
})"
<- function(category,period) {
Totals
%>%
total_estimates select(total_type,contains(period)) %>%
rename(emissions=2) %>%
filter(total_type==category) %>%
transmute(emissions=round(emissions,2)) %>%
paste0(.,ifelse(period=="year"," /yr",
ifelse(period=="month"," /mo"," /wk")))
}
<- function(category,period) {
SubTotals
%>%
household_estimates select(categories,subcategories,contains(period)) %>%
rename(emissions=3) %>%
filter(categories==category) %>%
arrange(subcategories) %>%
group_by(subcategories) %>%
summarize(emissions=sum(round(emissions,2))) %>%
rename(Subcategories=subcategories,Emissions=emissions)
}
<- function (id,active_ind=FALSE) {
accordionTitleContentUI tagList(
div(class = ifelse(active_ind==TRUE,"title active","title"),
h4(class="ui dividing header",textOutput(NS(id,"Total")))
),div(class = ifelse(active_ind==TRUE,"content active","content"),
tableOutput(NS(id,"SubTotal"))
)
)
}
<- function(id,category,period) {
accordionTitleContentServer moduleServer(id, function(input,output,session) {
$Total <- renderText({
outputpaste0("Total ",category,": ",Totals(category=category,period=period))
})
$SubTotal <- renderTable({
outputSubTotals(category=category,period=period)
})
outputOptions(output, "SubTotal", suspendWhenHidden = FALSE)
})
}
<- function(id,period) {
accordionMasterUI tagList(
::tags$script(accordion_js),
shinydiv(class = "ui segment",
h3(paste(ifelse(period=="year","Yearly",
ifelse(period=="month","Monthly","Weekly")),
"Carbon Estimates")),
h3(class="ui grey sub header","Total CO2 Emissions"),
div(class="ui grid",
div(class="four column row",
div(class="column",
h2(class="ui dividing header",textOutput(NS(id,"GrandTotal")))),
div(class="column",
strong(
ifelse(period=="year"," /year",
ifelse(period=="month"," /month"," /week"))
)
)
)),br(),
div(class="ui accordion",
accordionTitleContentUI(NS(id,"Travel"),active_ind=TRUE),
accordionTitleContentUI(NS(id,"Housing"),active_ind=FALSE)
)
)
)
}
<- function(id,period) {
accordionMasterServer moduleServer(id,function(input,output,session) {
$GrandTotal <- renderText({
output%>%
total_estimates select(total_type,contains(period)) %>%
rename(emissions=2) %>%
filter(total_type=="Grand") %>%
transmute(grandtotal=round(emissions,2)) %>%
paste0(.)
})
accordionTitleContentServer("Travel",category="Travel",period)
accordionTitleContentServer("Housing",category="Housing",period)
})
}
<- function() {
ui semanticPage(
h2("Our Accordions"),
div(class="ui stackable grid",
div(class="three wide column",
style="min-width: 350px;",
accordionMasterUI("accordion",period="month")
),div(class="four wide column",
h3("Totals by Category"),
div(class="ui raised segment",
style="min-width: 550px;",
tableOutput("TestTable")
)
)
)
)
}
<- function(input, output, session) {
server
accordionMasterServer("accordion",period="month")
$TestTable <- renderTable({
output
total_estimates
})
}
shinyApp(ui = ui(), server = server)
Adding Additional Categories
Now that we have our modules for the title and content written, let’s add the additional categories to our accordion. The three we need to add include food, goods, and services.
Down below, I simply call our accordionTitleContentUI
and accordionMasterServer
module functions with the new categories.
<- function(id,period) {
accordionMasterUI tagList(
::tags$script(accordion_js),
shinydiv(class = "ui segment",
h3(paste(ifelse(period=="year","Yearly",
ifelse(period=="month","Monthly","Weekly")),
"Carbon Estimates")),
h3(class="ui grey sub header","Total CO2 Emissions"),
div(class="ui grid",
div(class="four column row",
div(class="column",
h2(class="ui dividing header",textOutput(NS(id,"GrandTotal")))),
div(class="column",
strong(
ifelse(period=="year"," /year",
ifelse(period=="month"," /month"," /week"))
)
)
)),br(),
div(class="ui accordion",
accordionTitleContentUI(NS(id,"Travel"),active_ind=TRUE),
accordionTitleContentUI(NS(id,"Housing"),active_ind=FALSE),
accordionTitleContentUI(NS(id,"Food"),active_ind=FALSE),
accordionTitleContentUI(NS(id,"Goods"),active_ind=FALSE),
accordionTitleContentUI(NS(id,"Services"),active_ind=FALSE)
)
)
)
}
<- function(id,period) {
accordionMasterServer moduleServer(id,function(input,output,session) {
$GrandTotal <- renderText({
output%>%
total_estimates select(total_type,contains(period)) %>%
rename(emissions=2) %>%
filter(total_type=="Grand") %>%
transmute(grandtotal=round(emissions,2)) %>%
paste0(.)
})
accordionTitleContentServer("Travel",category="Travel",period)
accordionTitleContentServer("Housing",category="Housing",period)
accordionTitleContentServer("Food",category="Food",period)
accordionTitleContentServer("Goods",category="Goods",period)
accordionTitleContentServer("Services",category="Services",period)
}) }
Now when you run the application, you will see all categories included in the accordion now.
Improving the Titles for Each Accordion Section
Earlier, I added a temporary solution to our section headers. However, I don’t like how the category label and the metric bunch up together. We have a lot of unused white space to the right of it. I think we could do better.
Let’s change this so that the actual metric (“0.57 / mo”) floats to the right, while the label (“Total Travel:”) remains on the left side.
We’ll need to update the accordionTitleContentUI
and accordionTitleContentServer
modules to space this text apart.
In the accordionTitleContentUI
function, we can add a few div
functions to specify the grid and column setups we want. We’ll also create additional textOutput
functions for the “TitleHeader”.
<- function (id,active_ind) {
accordionTitleContentUI tagList(
div(class = ifelse(active_ind==TRUE,"title active","title"),
h4(class="ui dividing header",
div(class="ui grid",
div(class="two column row",
div(class="left floated column",
textOutput(NS(id,"TitleHeader"),inline=TRUE)),
div(class="right floated column",
style="text-align: right",
textOutput(NS(id,"Total")),inline=TRUE))
)
)
),div(class = ifelse(active_ind==TRUE,"content active","content"),
tableOutput(NS(id,"SubTotal"))
)
) }
In the accordionTitleContentServer
function, we’ll need to break apart the text into separate renderText
functions:
<- function(id,category,period) {
accordionTitleContentServer moduleServer(id, function(input,output,session) {
$TitleHeader <- renderText({
outputpaste0("Total ",category,": ")
})
$Total <- renderText({
outputTotals(category=category,period=period)
})
$SubTotal <- renderTable({
outputSubTotals(category,period=period)
})
outputOptions(output, "SubTotal", suspendWhenHidden = FALSE)
}) }
Now when we run it, we see that one floats to the left and the other floats to the right.
Adding Background Color to the Content Section
Right now, our content section (which has the data table for the subcategories) looks flat. I think we could make it pop out with a background color.
To make this update, we simply update the accordionTitleContentUI
module with div(class="ui teal inverted segment",…)
.
<- function (id,active_ind) {
accordionTitleContentUI tagList(
div(class = ifelse(active_ind==TRUE,"title active","title"),
h4(class="ui dividing header",textOutput(NS(id,"Total")))
),div(class = ifelse(active_ind==TRUE,"content active","content"),
div(class="ui teal inverted segment",
tableOutput(NS(id,"SubTotal"))
)
)
) }
Now our content sections standout from the title section above.
If you’re not a fan of the teal, try playing around with the segment settings. You can read about segments here: https://fomantic-ui.com/elements/segment.html
Removing Subcategory Labels and Filling Space
There are a few more tweaks we can make to our subcategory table.
For starters, we don’t really need the labels in the table. It’s already implied by other labels that we’re looking at the emissions of the subcategories of the category above them.
For that reason, we can remove the data set labels.
Second, the data set within the content section doesn’t use the space it’s allocated. It looks a little off center.
We can make both these updates with arguments in the renderTable
function.
Down below, we make this change in accordionTitleContentServer
, like so:
$SubTotal <- renderTable(width = "100%",colnames=FALSE,{
outputSubTotals(category,period=period)
})
When you compare this to the image in the last section, you can see how this looks differently:
Dictating Min and Max Accordion Widths
It’s a good idea to set a min and max width for the accordion so that it looks relatively consistent from app to app. Otherwise, the accordion may take up the entire screen on big monitors or look rather thin on small screens. You can do this with the style
argument for the senior div
function in accordionMasterUI
.
<- function(id,period) {
accordionMasterUI tagList(
::tags$script(accordion_js),
shinydiv(class = "ui segment",
style="min-width: 250px; max-width: 275px",
...
)
) }
Final Step: Showing All Three Accordions
We’re almost done! We built a functional set of accordion modules. We can show all three periods on our application.
Down below, I tweaked our original ui
function for the application to call the accordionMasterUI
and accordionMasterServer
module functions three times for year, month, and week. (Note: I also tweaked the column arrangement so the spacing works)
<- function() {
ui semanticPage(
h2("Our Accordions"),
div(class="ui stackable grid",
div(class="four wide computer eight wide tablet sixteen wide mobile column",
accordionMasterUI("year",period="year")
),div(class="four wide computer eight wide tablet sixteen wide mobile column",
accordionMasterUI("month",period="month")
),div(class="four wide computer eight wide tablet sixteen wide mobile column",
accordionMasterUI("week",period="week")
),div(class="two column row",
div(class="four wide column",
h3("Totals by Category"),
div(class="ui raised segment",
style="min-width: 550px;",
tableOutput("TestTable")
)
)
)
)
)
}
<- function(input, output, session) {
server
accordionMasterServer("year",period="year")
accordionMasterServer("month",period="month")
accordionMasterServer("week",period="week")
$TestTable <- renderTable({
output
total_estimates
})
}
shinyApp(ui = ui(), server = server)
Whenever we run this app, we see three accordions.
I hope you enjoyed this tutorial! Thanks for reading and feel free to reach out to Free State Analytics for R Shiny development or any other data science needs.