ETC5523: Communicating with Data
Workshop 10: Build a multi-tab Shiny dashboard
π― Objectives
By the end of this workshop, you will be able to:
- construct a
{bslib}navset_card_tab()from severalnav_panel()components; - connect outputs in different tabs to shared reactive calculations;
- use the selected tab as a reactive input;
- move the user to a panel with
nav_select(); and - show controls only when they are relevant to the active tab.
You will turn the Week 10 solar app into a four-tab dashboard. Three tabs reserve space for different views of the results. A fourth tab exposes an interval-by-interval calculation trace.
The focus today is the technical structure of a multi-tab app. The tutorial will build the detailed content inside the tabs.
Preparation
Download the tutorial starter app and household energy data. Save them in the same folder.
Make a copy of starter_app.R called workshop_app.R. Work in the copy so that you can compare your code with the supplied starter later.
Run the app once before changing anything.
The app uses library(bslib). All layout components in this workshop are standard functions documented on the {bslib} website.
Run the app after every stage. If it stops working, the most recent edit is the first place to look.
1. Trace the existing reactive structure (5 minutes)
Locate these three reactive expressions in server():
scenario_data()
metrics()
daily_data()Draw arrows showing which reactive calls which:
inputs Β·
scenario_data()Β·metrics()Β·daily_data()Β· outputs
Then answer:
- Which reactive performs the interval solar and battery calculations?
- Which reactive aggregates interval rows into daily rows?
- If the system-size slider changes, which objects become invalidated?
- Why should a tab call these reactives instead of rebuilding the scenario itself?
3. Connect the neutral tab to existing outputs (8 minutes)
The starter server already creates:
output$neutral_headline;output$neutral_message; andoutput$neutral_plot.
Place the matching output functions inside the Energy balance tab:
h3(textOutput("neutral_headline", inline = TRUE))
textOutput("neutral_message")
plotOutput("neutral_plot", height = "430px")Run the app and change the system size and date range.
For each of the three outputs, write down:
| UI placeholder | Server renderer | Shared reactive used |
|---|---|---|
textOutput("neutral_headline") |
||
textOutput("neutral_message") |
||
plotOutput("neutral_plot") |
Deliberately change one UI ID to "neutral_plots". Run the app, observe the empty space, and then repair the ID.
4. Add a calculation-trace tab (9 minutes)
The Evidence tab will help you inspect the calculation that feeds every other tab.
Add this placeholder to that tab:
tableOutput("calculation_trace")In server(), create output$calculation_trace with renderTable(). It should:
- call
scenario_data(); - keep the first eight rows; and
- display the time, observed consumption, modelled solar, net demand, battery state, grid import and grid export.
Round numeric values to two decimal places.
Change the date range and confirm that the first timestamp in the trace changes.
5. Treat the selected tab as an input (7 minutes)
Because the tabset has an ID, Shiny stores the selected tab in input$dashboard_tab.
Add this immediately before navset_card_tab(), then render its value in the server:
verbatimTextOutput("active_tab")
output$active_tab <- renderText({
paste("Active tab:", input$dashboard_tab)
})Now add an action button in the page body, immediately before navset_card_tab():
actionButton("show_evidence", "Inspect the calculation")Use observeEvent() and nav_select() to move to the Evidence panel when the button is clicked.
Test the button from each of the other three tabs.
6. Show a control only on the relevant tab (6 minutes)
Add this inside sidebar():
conditionalPanel(
condition = "input.dashboard_tab == 'evidence'",
sliderInput(
"trace_rows",
"Rows in calculation trace",
min = 4,
max = 48,
value = 8,
step = 4
)
)Update the calculation table so that it uses input$trace_rows instead of always showing eight rows.
Confirm that the slider appears only on the Evidence tab and that it changes the number of table rows.
7. Technical check before the tutorial (3 minutes)
Test the finished app systematically:
| Test | Expected result |
|---|---|
| Select each tab | The correct panel appears |
| Change system size | Neutral outputs and trace update |
| Change date range | Plot, message and trace update |
| Click Inspect the calculation | Evidence becomes active |
| Move the trace-row slider | Table length changes |
| Exclude the battery | Battery state becomes zero |
Then search your server code for add_solar_model( and dispatch_battery(. Each should be called only inside scenario_data().
A tab is a container, not a separate app. The dashboard should have one input state and one calculation pipeline, with several outputs reading from it.
Extension
Replace navset_card_tab() temporarily with another standard {bslib} navset:
navset_pill(
id = "dashboard_tab",
nav_panel("Why invest", value = "sales", ...),
nav_panel("Energy balance", value = "neutral", ...),
nav_panel("Cautious case", value = "cautious", ...),
nav_panel("Evidence", value = "evidence", ...)
)The panel contents and server code do not change. This separation between navigation, content and calculation is one advantage of standard {bslib} components.
Workshop check-out
Complete these statements:
A tabset becomes reactive whenβ¦
UI and server outputs are connected byβ¦
Shared reactives are preferable to tab-specific calculations becauseβ¦