The activity bar only handles page navigation. The dashboard itself is built from regular bslib layouts and popular htmlwidgets.
This example checks:
- Cards, value boxes, and filling layouts inside a screen-filling panel.
- Plotly, Leaflet, Reactable, and a base R chart rendering together.
- Plotly initializes after the first UI flush to avoid Shinylive asset races.
Live app
The first load may take a moment. The application runs entirely in the browser through WebAssembly.
Source code
library(shiny)
library(bslib)
library(bscode)
library(plotly)
required_packages <- c("leaflet", "reactable")
missing_packages <- required_packages[
!vapply(required_packages, requireNamespace, logical(1), quietly = TRUE)
]
if (length(missing_packages)) {
stop(
"This example requires: ",
paste(missing_packages, collapse = ", "),
"."
)
}
airports <- data.frame(
code = c("SCL", "LIM", "EZE", "GRU", "BOG", "MIA", "JFK", "MAD"),
city = c(
"Santiago", "Lima", "Buenos Aires", "Sao Paulo",
"Bogota", "Miami", "New York", "Madrid"
),
lat = c(-33.3930, -12.0219, -34.8222, -23.4356, 4.7016, 25.7959, 40.6413, 40.4983),
lng = c(-70.7858, -77.1143, -58.5358, -46.4731, -74.1469, -80.2870, -73.7781, -3.5676),
flights = c(312, 84, 91, 76, 48, 35, 22, 19),
on_time = c(0.84, 0.79, 0.76, 0.81, 0.86, 0.74, 0.72, 0.83)
)
routes <- airports[airports$code != "SCL", ]
origin <- airports[airports$code == "SCL", ]
route_lines <- do.call(
rbind,
lapply(seq_len(nrow(routes)), function(index) {
data.frame(
route = paste(origin$code, routes$code[index], sep = "–"),
lng = c(origin$lng, routes$lng[index]),
lat = c(origin$lat, routes$lat[index])
)
})
)
flights <- data.frame(
flight = c("LA2696", "LA2378", "LA8036", "LA500", "LA602", "LA630", "LA706", "LA532"),
destination = c("LIM", "EZE", "GRU", "MIA", "BOG", "JFK", "MAD", "LIM"),
departure = c("08:10", "09:25", "10:40", "11:15", "12:30", "13:05", "14:20", "15:10"),
gate = c("12", "7", "18", "21", "9", "15", "24", "6"),
status = c("Boarding", "On time", "Delayed", "On time", "Boarding", "Delayed", "On time", "On time"),
delay_min = c(0, 0, 28, 0, 0, 42, 0, 0)
)
ui <- page_bscode(
title = "Flight Operations",
id = "main_nav",
nav_panel(
"Dashboard",
value = "dashboard",
icon = icon("plane-departure"),
layout_columns(
col_widths = c(4, 4, 4, 6, 6, 7, 5),
row_heights = c("115px", "minmax(260px, 1fr)", "minmax(260px, 1fr)"),
fill = TRUE,
class = "p-3",
value_box(
"Flights today",
sum(airports$flights),
showcase = icon("plane"),
theme = "primary"
),
value_box(
"Destinations",
nrow(routes),
showcase = icon("location-dot")
),
value_box(
"On-time rate",
paste0(round(weighted.mean(airports$on_time, airports$flights) * 100), "%"),
showcase = icon("clock")
),
card(
full_screen = TRUE,
card_header("Route network"),
plotlyOutput("globe", height = "100%")
),
card(
full_screen = TRUE,
card_header("Airport map"),
leaflet::leafletOutput("airport_map", height = "100%")
),
card(
full_screen = TRUE,
card_header("Departures from SCL"),
reactable::reactableOutput("departures", height = "100%")
),
card(
full_screen = TRUE,
card_header("On-time performance"),
plotOutput("punctuality", height = "100%")
)
)
),
nav_spacer(),
nav_panel(
"Notes",
value = "notes",
icon = icon("circle-info"),
div(
class = "p-4",
h2("A dashboard made from ordinary components"),
p(
"The activity bar only provides page navigation. Cards, layouts, ",
"themes and htmlwidgets remain standard bslib and Shiny components."
),
tags$ul(
tags$li("Plotly renders the interactive route globe."),
tags$li("Leaflet renders the interactive airport map."),
tags$li("Reactable renders the searchable departures table."),
tags$li("Base R renders the punctuality chart.")
)
)
)
)
server <- function(input, output, session) {
plotly_ready <- reactiveVal(FALSE)
session$onFlushed(
function() plotly_ready(TRUE),
once = TRUE
)
output$globe <- renderPlotly({
req(plotly_ready())
plot_geo() |>
add_lines(
data = route_lines,
x = ~lng,
y = ~lat,
split = ~route,
hoverinfo = "text",
text = ~route,
line = list(width = 1)
) |>
add_markers(
data = airports,
x = ~lng,
y = ~lat,
text = ~paste0(code, " · ", city, "<br>", flights, " flights"),
hoverinfo = "text",
marker = list(size = 7)
) |>
layout(
geo = list(
projection = list(type = "orthographic"),
showland = TRUE,
showcountries = TRUE,
showocean = TRUE
),
margin = list(l = 0, r = 0, b = 0, t = 0),
showlegend = FALSE
)
})
output$airport_map <- leaflet::renderLeaflet({
map <- leaflet::leaflet(airports) |>
leaflet::addProviderTiles(leaflet::providers$CartoDB.Positron) |>
leaflet::addCircleMarkers(
lng = ~lng,
lat = ~lat,
radius = ~pmax(5, sqrt(flights)),
stroke = TRUE,
fillOpacity = 0.8,
label = ~paste0(code, " · ", city),
popup = ~paste0(
"<strong>", code, " · ", city, "</strong><br>",
flights, " flights<br>",
round(on_time * 100), "% on time"
)
)
for (index in seq_len(nrow(routes))) {
map <- leaflet::addPolylines(
map,
lng = c(origin$lng, routes$lng[index]),
lat = c(origin$lat, routes$lat[index]),
weight = 1.5,
opacity = 0.65
)
}
leaflet::fitBounds(
map,
lng1 = min(airports$lng) - 5,
lat1 = min(airports$lat) - 5,
lng2 = max(airports$lng) + 5,
lat2 = max(airports$lat) + 5
)
})
output$departures <- reactable::renderReactable({
reactable::reactable(
flights,
searchable = TRUE,
striped = TRUE,
highlight = TRUE,
compact = TRUE,
pagination = FALSE,
defaultSorted = "departure",
columns = list(
flight = reactable::colDef(name = "Flight"),
destination = reactable::colDef(name = "To"),
departure = reactable::colDef(name = "Departure"),
gate = reactable::colDef(name = "Gate", align = "center"),
status = reactable::colDef(name = "Status"),
delay_min = reactable::colDef(
name = "Delay",
align = "right",
cell = function(value) if (value == 0) "—" else paste(value, "min")
)
)
)
})
output$punctuality <- renderPlot({
barplot(
height = routes$on_time * 100,
names.arg = routes$code,
ylim = c(0, 100),
border = NA,
col = "#007ACC",
ylab = "On time (%)",
xlab = "",
las = 1
)
abline(h = 80, lty = 2, col = "#62676C")
}, res = 96)
}
shinyApp(ui, server)