library(shiny)
library(bslib)
library(dplyr)
library(lubridate)
library(plotly)
library(chiflights22)
library(histoslider)
library(rlang)
# Data prep
flights <- flights %>%
left_join(
airports %>%
transmute(dest_name = paste0(name, " (", faa, ")"), dest = faa, end_lat = lat, end_lon = lon)
) %>%
filter(!is.na(dest_name)) %>%
left_join(
airports %>% select(origin = faa, start_lat = lat, start_lon = lon)
) %>%
left_join(airlines, by = "carrier") %>%
rename(carrier_name = name) %>%
select(-time_hour) %>%
left_join(weather) %>%
mutate(
# The overwhelming majority of precipation is 0 so transform
# with some help from MASS::boxcox() https://stats.stackexchange.com/a/1452/48604
precip = scales::rescale(precip^-1.55/-.55),
date = lubridate::ymd(paste(year, month, day, sep = "-"))
)
SLIDER_HEIGHT <- 150
CHOICES <- list(
origin = c(
"Choose origin(s)" = "",
"O'Hare" = "ORD",
"Midway" = "MDW",
"Rockford" = "RFD"
),
dest_name = c(
"Choose destination(s)" = "",
sort(unique(flights$dest_name))
),
carrier_name = c(
"Choose carrier(s)" = "",
unique(flights$carrier_name)
)
)
sidebar_acc <- accordion(
open = c("Origin", "Destination"),
accordion_panel(
"Flight Path",
# See https://github.com/rstudio/fontawesome/issues/114
icon = fontawesome::fa("plane-departure"),
uiOutput("flight_path_reset"),
selectizeInput(
"origin", "Origin",
choices = CHOICES$origin,
multiple = TRUE,
options = list(plugins = "remove_button", closeAfterSelect = TRUE)
),
selectizeInput(
"dest_name", "Destination",
choices = CHOICES$dest_name,
multiple = TRUE,
options = list(plugins = "remove_button", closeAfterSelect = TRUE)
),
selectizeInput(
"carrier_name", "Carrier",
choices = CHOICES$carrier_name,
multiple = TRUE,
options = list(plugins = "remove_button", closeAfterSelect = TRUE)
)
),
accordion_panel(
"Flight time",
icon = fontawesome::fa("clock"),
input_histoslider(
"sched_dep_time", "Departure time",
flights$sched_dep_time,
height = SLIDER_HEIGHT,
options = list(handleLabelFormat = "0d")
),
input_histoslider(
"sched_arr_time", "Arrival time",
flights$sched_arr_time,
height = SLIDER_HEIGHT,
options = list(handleLabelFormat = "0d")
),
input_histoslider(
"date", "Date",
flights$date,
height = SLIDER_HEIGHT,
breaks = "months",
options = list(handleLabelFormat = "%b %e")
)
),
accordion_panel(
"Weather",
icon = fontawesome::fa("cloud-rain"),
# TODO: problematic (many NAs)
#input_histoslider(
# "precip", "Precipitation",
# flights$precip,
# height = SLIDER_HEIGHT
#),
input_histoslider(
"wind_speed", "Wind speed",
flights$wind_speed,
height = SLIDER_HEIGHT
),
input_histoslider(
"wind_gust", "Wind gust",
flights$wind_gust,
height = SLIDER_HEIGHT
)
)
)
flights_card <- card(
full_screen = TRUE,
card_header(
"Flight paths",
tooltip(
bsicons::bs_icon("info-circle", title = "About marker areas"),
"Marker areas are proportional to mean arrival delay"
),
class = "d-flex justify-content-between align-items-center"
),
plotlyOutput("flight_paths")
)
avg_delay_by_category <- card(
full_screen = TRUE,
card_header(
"Average delay by category",
popover(
bsicons::bs_icon("gear", title = "Settings"),
selectInput(
"avg_delay_category", "Category",
c("Carrier", "Month", "Weekday")
),
radioButtons(
"avg_delay_type", "Delay type",
c("Arrival", "Departure"),
inline = TRUE
)
),
class = "d-flex justify-content-between align-items-center"
),
plotlyOutput("scatter_delay")
)
delay_dist <- navset_card_underline(
title = "Distribution of delay times",
full_screen = TRUE,
id = "delay_dist_nav",
sidebar = sidebar(
position = "right",
open = FALSE,
radioButtons(
"delay_dist_type", "Delay type",
c("Arrival" = "arr_delay", "Departure" = "dep_delay"),
inline = TRUE
),
conditionalPanel(
"input.delay_dist_nav === 'Overall'",
selectizeInput(
"delay_dist_category", "Split by",
c("Choose a category" = "", "Carrier", "Month", "Weekday"),
options = list(plugins = "remove_button")
)
)
),
nav_panel(
"Overall",
plotlyOutput("delay_dist")
),
nav_panel(
"Over time",
plotlyOutput("arr_delay_series")
)
)
PRIMARY <- "#0675DD"
ui <- page_navbar(
theme = bs_theme(
preset = "shiny",
"primary" = PRIMARY
),
lang = "en",
title = tags$span(
tags$img(
src = "logo.png",
width = "46px",
height = "auto",
class = "me-3",
alt = "Shiny hex logo"
),
"Chicago Flights"
),
sidebar = sidebar(width = 275, sidebar_acc),
nav_spacer(),
nav_panel(
"Delay overview",
uiOutput("value_boxes"),
layout_columns(
flights_card, avg_delay_by_category
),
delay_dist
),
nav_panel(
"Data export",
card(
card_header("Flight data"),
DT::dataTableOutput("export")
)
),
nav_item(
tags$a(
tags$span(
bsicons::bs_icon("code-slash"), "Source code"
),
href = "https://github.com/rstudio/bslib/tree/main/inst/examples/flights",
target = "_blank"
)
),
nav_item(
input_dark_mode(id = "dark_mode", mode = "light")
)
)
server <- function(input, output, session) {
# ---------------------------------------------------------
# Flights tab logic
#
# WARNING: this server-side filtering logic is VERY experimental
# at this point and won't be easily adapt to different use cases.
# If you feel tempted to use it, use with caution.
# ---------------------------------------------------------
# Mapping from input id name to updating input function
input_discrete_vars <- list(
origin = updateSelectInput,
dest_name = updateSelectInput,
carrier_name = updateSelectInput
)
input_numeric_vars <- list(
sched_dep_time = update_histoslider,
sched_arr_time = update_histoslider,
date = update_histoslider
)
input_vars <- c(
input_discrete_vars,
input_numeric_vars
)
filter_index <- function(d) {
idx <- rep(TRUE, nrow(d))
for (var in names(d)) {
idx <- idx & filter_col(d, var)
}
idx & !is.na(idx)
}
filter_col <- function(d, var) {
vals <- d[[var]]
input_val <- input[[var]]
if (is.null(input_val) || identical(input_val, "")) {
return(TRUE)
}
if (is.character(vals) || is.factor(vals) || is.logical(vals)) {
return(d[[var]] %in% input_val)
}
# N.B. between() will remove NAs, which we probably don't
# want to remove until the slider is considered 'active'
rng <- range(vals, na.rm = TRUE)
active <- isTRUE(rng[1] <= input_val[1] || input_val[2] <= rng[2])
if (!active) {
return(TRUE)
}
dplyr::between(vals, input_val[1], input_val[2])
}
# Set up a listener for each input that effectively says update
# every other input when my value changes
lapply(names(input_vars), function(var) {
# We don't want updates to other variables to then
# cause an update to this variable
do_update <- reactiveVal(TRUE)
observeEvent(input[[var]], ignoreInit = TRUE, ignoreNULL = FALSE, {
if (!do_update()) return()
do_update(FALSE)
on.exit(do_update(TRUE), add = TRUE)
d <- flights[filter_index(flights), ]
if (nrow(d) == 0) return()
other_vars <- setdiff(names(input_vars), var)
lapply(other_vars, function(v) {
input_val <- input[[v]]
update_input_func <- input_vars[[v]]
if (v %in% names(input_discrete_vars)) {
choices <- CHOICES[[v]] %||% sort(unique(d[[v]]))
selected <- input_val %||% CHOICES[[v]][CHOICES[[v]] == ""]
update_input_func(
inputId = v, choices = choices,
selected = selected
)
} else {
update_input_func(
id = v,
values = d[[v]],
start = input_val[1],
end = input_val[2]
)
}
})
})
})
output$flight_path_reset <- renderUI({
req(c(input$origin, input$dest_name, input$carrier_name))
actionLink(
"flight_path_reset", "Reset",
style = htmltools::css(
position = "absolute",
right = "1rem",
text_decoration = "none",
font_weight = 700,
font_size = ".875rem"
)
)
})
observeEvent(input$flight_path_reset, {
updateSelectInput(
inputId = "origin",
choices = CHOICES$origin
)
updateSelectInput(
inputId = "dest_name",
choices = CHOICES$dest_name
)
updateSelectInput(
inputId = "carrier_name",
choices = CHOICES$carrier_name
)
})
# Flights with all filters applied (i.e., data used for value boxes/plots)
flight_dat <- reactive({
flights[filter_index(flights), ]
})
summary_vals <- reactive({
d <- flight_dat()
list(
n = scales::comma(nrow(d)),
n_dest = length(unique(d$dest_name)),
n_carriers = length(unique(d$carrier_name)),
dep_delay = round(mean(d$dep_delay, na.rm = T), 0),
dep_delay_perc = round(100 * sum(d$dep_delay > 0, na.rm = T) / nrow(d), 1),
arr_delay = round(mean(d$arr_delay, na.rm = T), 0),
arr_delay_perc = round(100 * sum(d$arr_delay > 0, na.rm = TRUE) / nrow(d), 1)
)
})
output$value_boxes <- renderUI({
vals <- summary_vals()
n_flights <- value_box(
"A TOTAL OF",
paste(vals$n, "flights"),
paste("Across", vals$n_dest, "destinations"),
tags$p(paste(
"On", vals$n_carriers, "different carriers"
)),
showcase = bsicons::bs_icon("airplane")
)
late <- if (vals$dep_delay > 0) "late" else "early"
delay_dep <- value_box(
"AVERAGE DEPARTURE",
paste(vals$dep_delay, "mins", late),
paste0(vals$dep_delay_perc, "% of flights depart ", late),
showcase = bsicons::bs_icon("hourglass-split")
)
late <- if (vals$arr_delay > 0) "late" else "early"
delay_arr <- value_box(
"AVERAGE ARRIVAL",
paste(vals$arr_delay, "mins", late),
paste0(vals$arr_delay_perc, "% of flights arrive ", late),
showcase = bsicons::bs_icon("hourglass-bottom")
)
layout_columns(class = "mb-0", n_flights, delay_dep, delay_arr)
}) %>%
bindCache(flight_dat())
plotly_base <- function(..., geo = FALSE, color = I(PRIMARY)) {
plot_func <- if (geo) plot_geo else plot_ly
plot_func(..., color = color) %>%
plotly::config(displayModeBar = FALSE) %>%
plotly::layout(
font = list(
family = "Open Sans",
color = if (input$dark_mode == "dark") "white" else "#1D1F21"
),
plot_bgcolor = "transparent",
paper_bgcolor = "transparent"
)
}
output$flight_paths <- renderPlotly({
flight_dat() %>%
group_by(start_lon, start_lat, end_lon, end_lat, origin, dest) %>%
summarise(mean_delay = mean(arr_delay, na.rm = TRUE)) %>%
plotly_base(geo = TRUE, showlegend = FALSE) %>%
add_segments(
x = ~start_lon, xend = ~end_lon,
y = ~start_lat, yend = ~end_lat,
alpha = 0.5, size = I(1), hoverinfo = "none"
) %>%
add_markers(
x = ~end_lon, y = ~end_lat, size = ~mean_delay,
hoverinfo = "text", alpha = 0.1,
text = ~paste0(
origin, " -> ", dest, "
",
"Average delay: ", round(mean_delay, 1)
)
) %>%
layout(
geo = list(
bgcolor = "transparent",
projection = list(
type = 'orthographic',
rotation = list(lon = -100, lat = 40, roll = 0)
),
showland = TRUE,
landcolor = toRGB("gray95"),
countrycolor = toRGB("gray80")
)
)
}) %>%
bindCache(flight_dat(), input$dark_mode)
output$scatter_delay <- renderPlotly({
d <- flight_dat()
req(nrow(d) > 0)
d <- switch(
input$avg_delay_category,
Weekday = group_by(d, y = lubridate::wday(date, label = TRUE)),
Month = group_by(d, y = lubridate::month(date, label = TRUE)),
Carrier = group_by(d, y = carrier_name),
stop("Category of ", input$avg_delay_category, "not implemented")
)
d <- switch(
input$avg_delay_type,
Arrival = summarise(d, avg = mean(arr_delay, na.rm = TRUE)),
Departure = summarise(d, avg = mean(dep_delay, na.rm = TRUE)),
)
d %>%
arrange(avg) %>%
mutate(y = factor(y, levels = y)) %>%
plotly_base(x = ~avg, y = ~y) %>%
add_bars(hoverinfo = "x") %>%
layout(
yaxis = list(title = ""),
xaxis = list(
title = paste("Average", tolower(input$avg_delay_type), "delay"),
hoverformat = ".1f",
gridcolor = if (input$dark_mode == "dark") "#303030"
)
)
}) %>%
bindCache(flight_dat(), input$dark_mode, input$avg_delay_category, input$avg_delay_type)
output$delay_dist <- renderPlotly({
d <- flight_dat()
x <- d[[input$delay_dist_type]]
req(length(x) > 0)
x_mean <- mean(x, na.rm = TRUE)
end <- quantile(x, probs = 0.99, na.rm = TRUE)
color <- switch(
input$delay_dist_category,
Carrier = d$carrier_name,
Month = lubridate::month(d$date, label = TRUE),
Weekday = lubridate::wday(d$date, label = TRUE),
I(PRIMARY)
)
plotly_base(
x = x, color = color,
hovertemplate = "%{y} flights were
%{x} min late "
) %>%
rangeslider(start = min(x, na.rm = TRUE), end = as.numeric(end)) %>%
add_annotations(
text = paste(
"Average",
switch(
input$delay_dist_type,
arr_delay = "arrival",
dep_delay = "departure"
),
"
delay of",
round(x_mean, 1), "min"
),
x = x_mean, y = 0.5, yref = "paper",
ax = 80, ay = -50,
font = list(size = 14)
) %>%
layout(
barmode = "stack",
yaxis = list(gridcolor = if (input$dark_mode == "dark") "#303030"),
shapes = list(
type = "line",
x0 = x_mean, x1 = x_mean,
y0 = 0, y1 = 1,
yref = "paper",
line = list(color = "lightgray", dash = "dash")
)
)
}) %>%
bindCache(flight_dat(), input$dark_mode, input$delay_dist_type, input$delay_dist_category)
output$arr_delay_series <- renderPlotly({
d <- flight_dat()
req(nrow(d) > 0)
d <- group_by(d, date)
d <- switch(
input$delay_dist_type,
arr_delay = summarise(d, y = mean(arr_delay, na.rm = TRUE)),
dep_delay = summarise(d, y = mean(dep_delay, na.rm = TRUE))
)
color <- switch(
input$delay_dist_category,
Carrier = d$carrier_name,
Month = lubridate::month(d$date, label = TRUE),
Weekday = lubridate::wday(d$date, label = TRUE),
I(PRIMARY)
)
plotly_base(
x = d$date, y = d$y, color = color,
hovertemplate = "%{y:.1f}",
) %>%
add_lines() %>%
layout(
hovermode = "x",
xaxis = list(title = "", tickformat = "%b %e"),
yaxis = list(title = "Average delay", showgrid = FALSE)
)
}) %>%
bindCache(flight_dat(), input$dark_mode, input$delay_dist_type, input$delay_dist_category)
output$export <- DT::renderDataTable({
DT::datatable(flight_dat(), fillContainer = TRUE)
})
}
shinyApp(ui, server)