Posit Table Contest Submission: Amtrak System

Published

December 22, 2022

The following is a repost of my submission in the 2022 Posit Table Contest, which was recognized as one of the runners up in the contest. The original tutorial, stand-alone table, and code repository are available if interested. Huge thanks to the Posit team for hosting this contest.

Table displaying Amtrak routes

Take me directly to the full table in Section 5.

1 Introduction

In this tutorial, I’ll walkthrough the process of developing a table in R using the reactable and reactablefmtr packages and other supplemental packages and functions. The goal is to create a table that describes the Amtrak system, with detail about the routes and the stations. The table displays a route on each row with the included stations along that route available as expandable details. To help visualize the stations and routes, the table includes embedded graphics generated by ggplot2.

1.1 Load packages and prepared data

The data used for this table comes from multiple sources including the US Department of Transportation, Wikipedia, and TrainWeb.org. The data preparation script and the generated data files are available in the Github repository.

Show the code
library(tidyverse)
library(reactable)
library(reactablefmtr)
library(sf)
library(lwgeom)
library(viridis)
library(ggbump)
library(tigris)
library(htmltools)

stations_df <- read_rds(here::here("data", "amtrak", "stations.rds"))
routes_df <- read_rds(here::here("data", "amtrak", "routes.rds"))
riders_df <- read_rds(here::here("data", "amtrak", "riders.rds"))

1.2 Prepare state/provincial boundary data

To generate maps of the United States and Canada, the tigris package provides US Census Bureau boundaries of states and the canadianmaps package includes data on provinces.

Show the code
states_sf <- 
  states(cb = T, progress_bar = F) %>% 
  filter(
    STUSPS %in% c(state.abb, "DC"),
    !STUSPS %in% c("HI", "AK")) %>% 
  select(ST_PROV = STUSPS)

sf_use_s2(FALSE)

provs_sf <- 
  canadianmaps::PROV %>% 
  st_make_valid() %>% 
  st_transform(crs = st_crs(states_sf)) %>% 
  select(ST_PROV = PT)

states_prov_sf <- 
  bind_rows(
    states_sf,
    provs_sf
  )

2 Routes

We can see that the routes data set includes all 43 active Amtrak routes and 9 variables, such as the beginning and ending cities, passenger volume in fiscal year 2021, length, duration, and which kind of train cars the route includes.

Show the code
glimpse(routes_df)
Rows: 43
Columns: 9
$ name              <chr> "Northeast Regional", "Acela", "Pacific Surfliner", …
$ route             <chr> "Boston/Springfield-New York-Washington-Norfolk/Newp…
$ daily_round_trips <chr> "18 (weekday), 15 (weekend)", "16 (weekday), 4 (Sat)…
$ fy2021_passengers <dbl> 3960000, 897600, 841000, 613200, 434100, 394300, 354…
$ route_miles       <chr> "644 (Newport News);679 (Norfolk);682 (Roanoke)", "4…
$ time              <chr> "14h 0m", "6h 30m ", "8h 30m", "8h 25m", "6h 15m (Oa…
$ url               <chr> "https://en.wikipedia.org/wiki/Northeast_Regional", …
$ cars              <chr> "Business, Coach, Cafe", "First Class, Business, Caf…
$ geometry          <GEOMETRY [°]> MULTILINESTRING ((-77.47296..., MULTILINEST…

2.1 Generate route maps for the table

The route data set also includes route geometry that we can use for geospatial analysis. In the code below, we create the routes and routes_map dataframes which will be used for the table. routes_map is generated using the route_map_fcn function which generates a ggplot2 map graphic using the route, station, and state/province geometries. We also generated a separate map graphic for the table header (rendered below).

Show the code
routes <- 
  routes_df %>% 
  mutate(
    map_plot = NA
  ) %>% 
  arrange(desc(fy2021_passengers)) %>% 
  bind_cols(
    route_color = turbo(nrow(routes_df))
  ) %>% 
  st_drop_geometry() %>% 
  arrange(name)

route_map_fcn <- function(route_sf, stations_sf, states_sf) {
  
  route_mer <- st_set_geometry(route_sf, route_sf$geometry) %>% st_transform(crs = 3857)
  stations_mer <- stations_sf %>% st_transform(crs = 3857)
  states_mer <- states_sf %>% st_transform(crs = 3857)
  route_circle <- st_minimum_bounding_circle(st_simplify(route_mer, dTolerance = 1000))
  route_bbox <- st_bbox(route_circle) %>% st_as_sfc()
  route_name <- route_sf$name
  
  gg <- 
    (ggplot() +
       geom_sf(data = states_mer, linetype = "dashed", fill = "gray90", size = 0.25) +
       geom_sf(data = route_mer, aes(color = route_color), size = 1.75) +
       geom_sf(data = stations_mer, shape = 21, size = 1.25, fill = 'white', stroke = 0.5) +
       scale_color_identity() +
       coord_sf(
         xlim = st_coordinates(route_bbox)[c(1,2),1], # min & max of x values
         ylim = st_coordinates(route_bbox)[c(2,3),2]) + # min & max of y values
       theme_void() +
       theme(
         legend.position = 'none',
         panel.background = element_rect(fill = 'transparent', color = NA),
         plot.background = element_rect(fill = 'transparent', color = NA)))
  
  result <- tibble(route = route_name, plot = list(gg))
  
  return(result)
}

routes_stations_sf <- 
  stations_df %>% 
  filter(route != "Winter Park Express") %>% 
  group_by(route) %>% 
  summarize() %>% 
  ungroup() %>% 
  arrange(route)

routes_map_sf <-
  routes %>%
  inner_join(
    routes_df %>% select(name, geometry),
    by = "name") %>%
  select(
    name,
    route_color,
    geometry) %>% 
  arrange(name)

routes_map_sf <- st_set_geometry(routes_map_sf, routes_map_sf$geometry)

routes_map <-
  map2_dfr(
    .x = group_split(routes_map_sf, name),
    .y = group_split(routes_stations_sf, route),
    .f = ~route_map_fcn(route_sf = .x, stations_sf = .y, states_sf = states_prov_sf))

state_route_map <- 
  ggplot() +
  geom_sf(data = states_prov_sf, linetype = "dashed", fill = "gray90", linewidth = 0.25) +
  geom_sf(data = routes_map_sf, aes(color = route_color), linewidth = 1.5, alpha = 1.0) +
  geom_sf(data = routes_stations_sf, shape = 21, size = 1, fill = 'white', stroke = 0.50) +
  coord_sf(xlim = c(-124.763, -66.949), ylim = c(24.523, 51), expand = FALSE) +
  scale_color_identity() +
  theme_void() +
  theme(legend.position = 'none')

state_route_map

2.2 Create functions to generate icons representing available train cars

Each of the routes uses one or more different types of train cars (dining, coach, etc.). To display this information, we use the function below to replace the word descriptions with icons representing whether each type of train car is present for the route.

Show the code
icons <- function(icon, color, size = 30, empty = FALSE) {
  
  fill_color <- grDevices::adjustcolor(color, alpha.f = 1.0)
  empty_color <- grDevices::adjustcolor(color, alpha.f = 0.3)
  
  htmltools::tagAppendAttributes(
    shiny::icon(icon),
    style = paste0("font-size:", size, "px", "; color:", if (empty) empty_color else fill_color),
    "aria-hidden" = "true"
    )
}

train_icons <- function(vals) {
  
  if(is.na(vals)) {
    
    coach <- span(icons("train", "gray10", empty = T), title = "Coach Not Available", style = "margin: 5px;")
    diner <- span(icons("utensils", "gray10", empty = T), title = "Diner/Cafe Not Available", style = "margin: 5px;")
    sleeper <- span(icons("bed", "gray10", empty = T), title = "Sleeper Not Available", style = "margin: 5px;")
    business <- span(icons("briefcase", "gray10", empty = T), title = "Business Not Available", style = "margin: 5px;")
    first_class <- span(icons("money-check-dollar", "gray10", empty = T), title = "First Class Not Available", style = "margin: 5px;")
    auto <- span(icons("car-side", "gray10", empty = T), title = "Auto Transport Not Available", style = "margin: 5px;")
    
  } else {
  
    if (str_detect(vals, "Coach")) {
      coach <- span(icons("chair", "gray10", empty = F), title = "Coach Available", style = "margin: 5px;")
    } else {
      coach <- span(icons("chair", "gray10", empty = T), title = "Coach Not Available", style = "margin: 5px;")
    }
    if (str_detect(vals, "Dinner|Dinette|Cafe|Bistro")) {
      diner <- span(icons("utensils", "gray10", empty = F), title = "Diner/Cafe Available", style = "margin: 5px;")
    } else {
      diner <- span(icons("utensils", "gray10", empty = T), title = "Diner/Cafe Not Available", style = "margin: 5px;")
    }
    if (str_detect(vals, "Sleeper")) {
      sleeper <- span(icons("bed", "gray10", empty = F), title = "Sleeper Available", style = "margin: 5px;")
    } else {
      sleeper <- span(icons("bed", "gray10", empty = T), title = "Sleeper Not Available", style = "margin: 5px;")
    }
    if (str_detect(vals, "Business")) {
      business <- span(icons("briefcase", "gray10", empty = F), title = "Business Available", style = "margin: 5px;")
    } else {
      business <- span(icons("briefcase", "gray10", empty = T), title = "Business Not Available", style = "margin: 5px;")
    }
    if (str_detect(vals, "First Class")) {
      first_class <- span(icons("money-check-dollar", "gray10", empty = F), title = "First Class Available", style = "margin: 5px;")
    } else {
      first_class <- span(icons("money-check-dollar", "gray10", empty = T), title = "First Class Not Available", style = "margin: 5px;")
    }
    if (str_detect(vals, "Auto")) {
      auto <- span(icons("car-side", "gray10", empty = F), title = "Auto Transport Available", style = "margin: 5px;")
    } else {
      auto <- span(icons("car-side", "gray10", empty = T), title = "Auto Transport Not Available", style = "margin: 5px;")
    }
    
  }
  
  div(coach, diner, sleeper, business, first_class, auto)
  
}

2.3 Generate sample routes reactable table

We can now use the route data frames created above to generate a sample reactable table, using the first 5 rows of the routes data. In this table, we create a bar chart of passenger data using reactablefmtr, display the train car icons, include a map graphic, and format the other columns.

Show the code
reactable(
    data = routes %>% slice(1:5),
    highlight = TRUE,
    wrap = TRUE,
    defaultPageSize = 5,
    style = list(
      fontFamily = "Recursive, sans-serif", 
      fontSize = "0.875rem"),
    defaultColDef = colDef(
      vAlign = "center",
      align = "center",
      headerVAlign = "center",
      sortable = FALSE),
    width = 1250,
    defaultSorted = "fy2021_passengers",
    columns = list(
      name = colDef(
        name = "Route",
        align = "left",
        html = TRUE,
        cell = function(value, index) {
          rte <- tags$strong(tags$a(href = as.character(routes[index, "url"]), target = "_blank", value))
          rte_cities <- as.character(routes[index, "route"])
          cities <- div(style = list(float = "left", fontSize = "0.7rem"), rte_cities)
          if (value == "Adirondack") {
            sup <- tags$sup("*")
          } else if (value %in% c("Berkshire Flyer", "Valley Flyer")) {
            sup <- tags$sup("**") 
          } else {
            sup <- NULL
          }
          tagList(rte, sup, tags$br(), cities)
        },
        width = 250,
        sortable = TRUE
      ),
      daily_round_trips = colDef(
        name = "Daily Trips",
        html = TRUE,
        cell = function(value) {
          str_replace_all(value, "\\,", "<br>")
        },
        width = 150,
        style = list(fontSize = "0.8rem")
      ),
      fy2021_passengers = colDef(
        name = "Passengers (FY 2021)",
        defaultSortOrder = "desc",
        cell = data_bars(
          routes,
          fill_color_ref = "route_color",
          text_position = "above",
          number_fmt = scales::comma,
          background = "lightgray"
        ),
        width = 125,
        sortable = TRUE
      ),
      route_miles = colDef(
        name = "Distance (miles)",
        html = TRUE,
        cell = function(value) {
          str_replace_all(value, "\\;", "<br>")
        },
        width = 150,
        style = list(fontSize = "0.8rem")
      ),
      time = colDef(
        name = "Journey Time",
        html = TRUE,
        cell = function(value) {
          str_replace_all(value, "\\;", "<br>")
        },
        width = 150,
        style = list(fontSize = "0.8rem")
      ),
      cars = colDef(
        name = "Available Train Cars",
        cell = function(value) {
          train_icons(value)
        },
        width = 175
      ),
      map_plot = colDef(
        name = "Route Map",
        cell = function(value, index){
          htmltools::plotTag(
            routes_map$plot[[index]],
            alt = 'plots',
            height = 100,
            width = 100,
            deviceArgs = list(bg = 'transparent'))
          },
        width = 200
      ),
      route = colDef(show = FALSE),
      url = colDef(show = FALSE),
      route_color = colDef(show = FALSE)
    )
) %>% 
  div(
    .,
    style = css(
    'text-align' = 'center')
  )
Route
Daily Trips
Passengers (FY 2021)
Distance (miles)
Journey Time
Available Train Cars
Route Map
Acela
Boston-New York-Washington
16 (weekday)
4 (Sat)
9 (Sun)
897,600
456
6h 30m
plots
Auto Train
Lorton-Sanford
1
199,400
855
17h 30m
plots
Amtrak Cascades
Vancouver-Seattle-Portland-Eugene
4
181,500
467
3h 55m (Vancouver)
6h 10m (Eugene)
plots
Berkshire Flyer**
New York - Pittsfield
1 weekly round trip
NA
190
4h 0m
plots
Adirondack*
Montreal-New York
Suspended
NA
381
10h 50m
plots

3 Stations and ridership

The stations data set includes all 876 active Amtrak route/station combinations and variables such as the station location, what other routes the station serves, when the station opened, and what type of station it is. The data also includes a variable indicating what type of station junction it is along the route (beginning, middle, split, end, etc.).

The ridership data includes ridership by station from fiscal year 2005 to 2021.

Show the code
glimpse(stations_df)
Rows: 876
Columns: 13
$ route             <chr> "Acela", "Acela", "Acela", "Acela", "Acela", "Acela"…
$ stop_num          <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 1…
$ junction_type     <chr> "sta", "cont", "cont", "cont", "cont", "cont", "cont…
$ station_name      <chr> "Boston South (BOS)", "Boston Back Bay (BBY)", "West…
$ station_abbr      <chr> "BOS", "BBY", "RTE", "PVD", "NLC", "NHV", "STM", "NY…
$ state_or_province <chr> "MA", "MA", "MA", "RI", "CT", "CT", "CT", "NY", "NJ"…
$ country           <chr> "US", "US", "US", "US", "US", "US", "US", "US", "US"…
$ station_routes    <list> <"Lake Shore Limited", "Northeast Regional">, <"Lak…
$ other_routes      <chr> "Lake Shore Limited; Northeast Regional", "Lake Shor…
$ opened            <dbl> 1899, 1860, 1953, 1986, 1848, 1920, 1849, 1910, 1935…
$ station_type      <chr> "Station Building (with waiting room)", "Station Bui…
$ url               <chr> "https://en.wikipedia.org/wiki/South_Station", "http…
$ geometry          <POINT [°]> POINT (-71.0553 42.35232), POINT (-71.07583 42…
Show the code
glimpse(riders_df)
Rows: 8,908
Columns: 3
$ station_abbr <chr> "ABE", "ABE", "ABE", "ABE", "ABE", "ABE", "ABE", "ABE", "…
$ year         <int> 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 201…
$ riders       <dbl> 23438, 18008, 38702, 45052, 44495, 41114, 39878, 43987, 4…

3.1 Join station and ridership data and create subway-style route diagrams

Since the stations in a route may be along different branches, it’s helpful to display the data using a route diagram. Some Wikipedia articles (see example) include helpful subway-style route diagrams to display different types of junctions. The route_diagram_fcn below generate specific diagram for each type of junction in ggplot2 in the stations_diag dataframe, which will be used with the stations dataframe in the table.

Show the code
route_diagram_fcn <- function(node_type, line_color) {
  
  if (node_type == "1-2_split") {
    lines_df <-
      tibble(
        x = c(-1, 0, 0, 0),
        y = c(-3, 0, -3, 3),
        group = c(1, 1, 1, 1)
      )
  } else if (node_type == "1-2_split_aft") {
    lines_df <-
      tibble(
        x = c(-1, 0, 0, 0),
        y = c(-3, -1.5, -3, 3),
        group = c(1, 1, 1, 1)
      )
  } else if (node_type == "2-1_comb") {
    lines_df <-
      tibble(
        x = c(-1, 0, 0, 0),
        y = c(3, 0,-3, 3),
        group = c(1, 1, 1, 1)
      )
  } else if (node_type == "sta") {
    lines_df <-
      tibble(x = c(0, 0),
             y = c(0, -3),
             group = c(1, 1))
  } else if (node_type == "beg_bypass") {
    lines_df <-
      tibble(x = c(-1, 0, 0),
             y = c(-3, 0, 3),
             group = c(1, 1, 1))
  } else if (node_type == "cont") {
    lines_df <-
      tibble(x = c(0, 0, 0),
             y = c(0, 3,-3),
             group = c(1, 1, 1))
  } else if (node_type == "cont_aft_bypass") {
    lines_df <-
      tibble(x = c(-1, 0, 0),
             y = c(3, 0, -3),
             group = c(1, 1, 1))
  } else if (node_type == "cont_w_bypass") {
    lines_df <-
      tibble(x = c(0, 0, -1, -1),
             y = c(3, -3, 3, -3),
             group = c(1, 1, 2, 2))
  } else if (node_type == "end") {
    lines_df <-
      tibble(x = c(0, 0),
             y = c(0, 3),
             group = c(1, 1))
  } else if (node_type == "end_w_bypass") {
    lines_df <-
      tibble(x = c(0, 0, -1, -1),
             y = c(0, 3, 3, -3),
             group = c(1, 1, 2, 2))
  } else if (node_type == "sta_w_bypass") {
    lines_df <-
      tibble(x = c(0, 0, -1, -1),
             y = c(0, -3, 3, -3),
             group = c(1, 1, 2, 2))
  }
  
  pts_df <- 
  tibble(
    x = 0,
    y = 0
  )
  
  ggplot() +
    geom_bump(
      data = lines_df, 
      aes(x, y, group = group), 
      linewidth = 4, 
      color = line_color, 
      direction = "y") +
    geom_point(
      data = pts_df, 
      aes(x, y), 
      shape = 21, 
      size = 10, 
      stroke = 2,
      color = "gray20", 
      fill = "white") +
    scale_color_identity() +
    scale_x_continuous(expand = c(0, 0)) + 
    scale_y_continuous(expand = c(0, 0)) +
    coord_cartesian(xlim = c(-2, 2), ylim = c(-3, 3)) +
    theme_void()
}

riders <- 
  riders_df %>% 
  separate_rows(station_abbr, sep = "\\/") %>% 
  arrange(year) %>% 
  group_by(station_abbr) %>% 
  summarize(riders = list(riders)) %>% 
  ungroup()

stations <- 
  stations_df %>% 
  st_drop_geometry() %>% 
  inner_join(
    routes %>% select(route = name, route_color),
    by = "route"
  ) %>% 
  inner_join(
    tibble(
      state_or_province = c(state.abb, "DC", "ON", "QC", "BC"),
      st_prov_name = c(state.name, "District of Columbia", "Ontario", "Quebec", "British Columbia")
    ),
    by = "state_or_province"
  ) %>% 
  left_join(
    riders, by = "station_abbr"
  ) %>% 
  select(-station_abbr) %>% 
  transmute(
    route,
    route_color,
    junction_type,
    station_name,
    url,
    st_prov_name,
    country,
    riders = modify_if(riders, ~is.null(.), ~rep(NA_real_, 17)),
    opened,
    station_type,
    station_routes = map(station_routes, ~str_subset(.x, "Winter Park Express", negate = T)),
    other_routes = modify_if(station_routes, ~length(.) == 0, ~NA_character_),
    other_routes = map_chr(other_routes, ~glue::glue_collapse(.x, sep = "; "))
  )

stations_diag <-
  stations %>%
  select(route, station_name, junction_type, route_color) %>%
  mutate(PLOT = map2(junction_type, route_color, ~route_diagram_fcn(node_type = .x, line_color = .y)))

3.2 Generate sample stations reactable table

Similar to the routes table above, we can use the stations data frames created above to generate a sample reactable table, using the stations on the Northeast Regional route. In this table, we embed the route diagram for the station, create a sparkline chart of passenger data using reactablefmtr, display the connecting routes using a details breakout, and format the other columns.

Show the code
stat_data <- stations %>% filter(route == "Northeast Regional")
stat_route_color <- stat_data %>% pull(route_color) %>% unique()
stat_plot <- inner_join(stations_diag, stat_data, by = c("route", "station_name"))
      
reactable(
  stat_data,
  outlined = FALSE,
  theme = reactableTheme(
    cellPadding = "0px 6px",
    style = list(".rt-tr-details" = list("text-align" = "right"))
  ),
  style = list(fontFamily = "Recursive, sans-serif",
               fontSize = "0.875rem"),
  defaultPageSize = nrow(stat_data),
  defaultColDef = colDef(
    vAlign = "center",
    align = "center",
    headerVAlign = "center"
  ),
  width = 1250,
  sortable = FALSE,
  columns = list(
    junction_type = colDef(
      name = "",
      resizable = FALSE,
      align = "left",
      cell = function(value, index) {
        htmltools::plotTag(
          stat_plot$PLOT[[index]],
          alt = 'plots',
          height = 100,
          width = 100
        )
      },
      width = 100
    ),
    station_name = colDef(
      name = "Station",
      resizable = TRUE,
      align = "left",
      cell = function(value, index) {
        stat_url <-
          tags$a(href = as.character(stat_data[index, "url"]), target = "_blank", value)
        state <-
          as.character(stat_data[index, "st_prov_name"])
        country <- as.character(stat_data[index, "country"])
        flag_url <- paste0(
          "https://raw.githubusercontent.com/catamphetamine/country-flag-icons/master/flags/1x1/",
          country,
          ".svg"
        )
        flag_img <-
          image <-
          img(src = flag_url, style = "width:45px;height:15px;", alt = country)
        state_div <-
          div(style = list(float = "left", fontSize = "0.7rem"),)
        tagList(stat_url, tags$br(), state, flag_img)
      },
      minWidth = 250
    ),
    riders = colDef(
      name = "Station Ridership (FY 2005-2021)",
      cell = react_sparkline(
        stat_data,
        decimals = 0,
        tooltip_type = 2,
        height = 100,
        show_area = TRUE,
        line_width = 2,
        area_color_ref = "route_color",
        area_opacity = 0.5,
        margin = margin(10, 5, 10, 0)
      ),
      width = 450
    ),
    opened = colDef(
      name = "Year Opened (Rebuilt)",
      cell = color_tiles(
        data = stat_data,
        colors = stat_route_color %>% shades::saturation(seq(0.2, 1, 0.2)) %>% as.character(),
        opacity = 0.7,
        bold_text = FALSE,
        box_shadow = FALSE
      ),
      width = 150
    ),
    station_type = colDef(
      name = "Station Type",
      cell = function(value, index) {
        if (is.na(value)) {
          station_icon <- 'train'
        } else if (value == "Station Building (with waiting room)") {
          station_icon <- 'building-user'
        } else if (value == "Platform with Shelter") {
          station_icon <- 'people-roof'
        } else {
          station_icon <- 'train'
        }
        span(icons(station_icon, "gray10", empty = F),
             title = value,
             style = "margin: 5px;")
      },
      width = 125
    ),
    station_routes = colDef(
      name = "Connecting Routes",
      html = TRUE,
      cell = function(value, index) {
        if (length(value) == 0) {
          "NA"
        } else {
          paste0(length(value), " routes")
        }
      },
      details = function(index) {
        if (length(stat_data$station_routes[index][[1]]) > 0) {
          connections <- stat_data$other_routes[index]
          paste0("Connecting to: ", connections)
        }
      },
      width = 125
    ),
    route = colDef(show = FALSE),
    stop_num = colDef(show = FALSE),
    url = colDef(show = FALSE),
    state_or_province = colDef(show = FALSE),
    country = colDef(show = FALSE),
    other_routes = colDef(show = FALSE),
    route_color = colDef(show = FALSE),
    st_prov_name = colDef(show = FALSE)
  )
) %>% 
  div(
    .,
    style = css(
    'text-align' = 'center')
  )
Station
Station Ridership (FY 2005-2021)
Year Opened (Rebuilt)
Station Type
Connecting Routes
plots
Boston South (BOS)
MassachusettsUS
1899
2 routes
plots
1860
2 routes
plots
1953
1 routes
plots
Providence (PVD)
Rhode IslandUS
1986
1 routes
plots
Kingston (KIN)
Rhode IslandUS
1875
NA
plots
Westerly (WLY)
Rhode IslandUS
1837
NA
plots
Mystic (MYS)
ConnecticutUS
1858
NA
plots
New London (NLC)
ConnecticutUS
1848
1 routes
plots
1873
NA
plots
Springfield (SPG)
MassachusettsUS
1973
4 routes
plots
1831
3 routes
plots
Windsor (WND)
ConnecticutUS
1870
2 routes
plots
Hartford (HFD)
ConnecticutUS
1889
3 routes
plots
Berlin (BER)
ConnecticutUS
1839
3 routes
plots
Meriden (MDN)
ConnecticutUS
1839
3 routes
plots
1871
2 routes
plots
1920
4 routes
plots
Bridgeport (BRP)
ConnecticutUS
1840
1 routes
plots
Stamford (STM)
ConnecticutUS
1849
2 routes
plots
1987
NA
plots
1910
15 routes
plots
1935
10 routes
plots
2001
1 routes
plots
Metropark (MET)
New JerseyUS
1971
4 routes
plots
1838
2 routes
plots
1864
2 routes
plots
Trenton (TRE)
New JerseyUS
1863
10 routes
plots
NA
1 routes
plots
1870
1 routes
plots
1933
10 routes
plots
1908
8 routes
plots
Newark (NRK)
DelawareUS
1877
NA
plots
1898
NA
plots
1911
8 routes
plots
1980
3 routes
plots
1983
2 routes
plots
Washington Union (WAS)
District of ColumbiaUS
1908
9 routes
plots
1905
6 routes
plots
1992
NA
plots
1914
2 routes
plots
NA
2 routes
plots
1885
2 routes
plots
1912
1 routes
plots
1975
NA
plots
1992
NA
plots
1971
1 routes
plots
1910
2 routes
plots
1866
NA
plots
1975
4 routes
plots
2003
NA
plots
1935
NA
plots
1981
NA
plots
1955
4 routes
plots
2012
NA

4 Generate nested reactable table including routes and stations

Finally, we can bring together the routes and stations data into one reactable table. After creating the amtrak_table object, we add headers and footers using the prependContent and appendContent functions in the htmlwidgets package. The header includes the map graphic of the whole Amtrak system as well as the title and subtitle. The footer includes footnotes and source information.

Show the code
amtrak_table <- 
  reactable(
    data = routes,
    highlight = TRUE,
    wrap = TRUE,
    defaultPageSize = nrow(routes),
    style = list(
      fontFamily = "Recursive, sans-serif", 
      fontSize = "0.875rem"),
    defaultColDef = colDef(
      vAlign = "center",
      align = "center",
      headerVAlign = "center",
      sortable = FALSE),
    width = 1250,
    defaultSorted = "fy2021_passengers",
    columns = list(
      name = colDef(
        name = "Route",
        align = "left",
        html = TRUE,
        cell = function(value, index) {
          rte <- tags$strong(tags$a(href = as.character(routes[index, "url"]), target = "_blank", value))
          rte_cities <- as.character(routes[index, "route"])
          cities <- div(style = list(float = "left", fontSize = "0.7rem"), rte_cities)
          if (value == "Adirondack") {
            sup <- tags$sup("*")
          } else if (value %in% c("Berkshire Flyer", "Valley Flyer")) {
            sup <- tags$sup("**") 
          } else {
            sup <- NULL
          }
          tagList(rte, sup, tags$br(), cities)
        },
        width = 250,
        sortable = TRUE
      ),
      daily_round_trips = colDef(
        name = "Daily Trips",
        html = TRUE,
        cell = function(value) {
          str_replace_all(value, "\\,", "<br>")
        },
        width = 150,
        style = list(fontSize = "0.8rem")
      ),
      fy2021_passengers = colDef(
        name = "Passengers (FY 2021)",
        defaultSortOrder = "desc",
        cell = data_bars(
          routes,
          fill_color_ref = "route_color",
          text_position = "above",
          number_fmt = scales::comma,
          background = "lightgray"
        ),
        width = 125,
        sortable = TRUE
      ),
      route_miles = colDef(
        name = "Distance (miles)",
        html = TRUE,
        cell = function(value) {
          str_replace_all(value, "\\;", "<br>")
        },
        width = 150,
        style = list(fontSize = "0.8rem")
      ),
      time = colDef(
        name = "Journey Time",
        html = TRUE,
        cell = function(value) {
          str_replace_all(value, "\\;", "<br>")
        },
        width = 150,
        style = list(fontSize = "0.8rem")
      ),
      cars = colDef(
        name = "Available Train Cars",
        cell = function(value) {
          train_icons(value)
        },
        width = 175
      ),
      map_plot = colDef(
        name = "Route Map",
        cell = function(value, index){
          htmltools::plotTag(
            routes_map$plot[[index]],
            alt = 'plots',
            height = 100,
            width = 100,
            deviceArgs = list(bg = 'transparent'))
          },
        width = 200
      ),
      route = colDef(show = FALSE),
      url = colDef(show = FALSE),
      route_color = colDef(show = FALSE)
    ),
    details = function(index) {
      station_data <- stations[stations$route == routes$name[index], ]
      station_route_color <- station_data %>% pull(route_color) %>% unique()
      htmltools::div(
        style = "padding: 0rem",
        reactable(
          station_data, 
          outlined = FALSE,
          theme = reactableTheme(
            cellPadding = "0px 6px",
            style = list(".rt-tr-details" = list("text-align" = "right"))
          ),
          defaultPageSize = nrow(station_data),
          defaultColDef = colDef(
            vAlign = "center",
            align = "center",
            headerVAlign = "center"),
          width = 1250,
          sortable = FALSE,
          columns = list(
            junction_type = colDef(
              name = "",
              resizable = FALSE,
              align = "left",
              cell = function(value, index){
                station_plot <- inner_join(stations_diag, station_data, by = c("route", "station_name"))
                htmltools::plotTag(
                  station_plot$PLOT[[index]],
                  alt = 'plots',
                  height = 100,
                  width = 100)
                },
              width = 100
            ),
            station_name = colDef(
              name = "Station",
              resizable = TRUE,
              align = "left",
              cell = function(value, index) {
                stat_url <- tags$a(href = as.character(station_data[index, "url"]), target = "_blank", value)
                state <- as.character(station_data[index, "st_prov_name"])
                country <- as.character(station_data[index, "country"])
                flag_url <- paste0(
                  "https://raw.githubusercontent.com/catamphetamine/country-flag-icons/master/flags/1x1/", 
                  country, ".svg")
                flag_img <- image <- img(src = flag_url, style = "width:45px;height:15px;", alt = country)
                state_div <- div(style = list(float = "left", fontSize = "0.7rem"), )
                tagList(stat_url, tags$br(), state, flag_img)
                },
              minWidth = 250
              ),
            riders = colDef(
              name = "Station Ridership (FY 2005-2021)",
              cell = react_sparkline(
                station_data,
                decimals = 0,
                tooltip_type = 2,
                height = 100,
                show_area = TRUE,
                line_width = 2,
                area_color_ref = "route_color",
                area_opacity = 0.5,
                margin = margin(10, 5, 10, 0)
                ),
              width = 450),
            opened = colDef(
              name = "Year Opened (Rebuilt)",
               cell = color_tiles(
                 data = station_data,
                 colors = station_route_color %>% shades::saturation(seq(0.2, 1, 0.2)) %>% as.character(),
                 opacity = 0.7,
                 bold_text = FALSE,
                 box_shadow = FALSE
                 ),
              width = 150
              ),
            station_type = colDef(
              name = "Station Type",
              cell = function(value, index) {
                if (is.na(value)) {
                  station_icon <- 'train'
                } else if (value == "Station Building (with waiting room)") {
                  station_icon <- 'building-user'
                } else if (value == "Platform with Shelter") {
                  station_icon <- 'people-roof'
                } else {
                  station_icon <- 'train'
                }
                span(icons(station_icon, "gray10", empty = F), title = value, style = "margin: 5px;")
              },
              width = 125
            ),
            station_routes = colDef(
              name = "Connecting Routes",
              html = TRUE,
              cell = function(value, index) {
                if (length(value) == 0) {
                  "NA"
                } else {
                  paste0(length(value), " routes")
                }
              },
              details = function(index) {
                if (length(station_data$station_routes[index][[1]]) > 0) {
                  connections <- station_data$other_routes[index]
                  paste0("Connecting to: ", connections)
                  }
                },
              width = 125
              ),
            route = colDef(show = FALSE),
            stop_num = colDef(show = FALSE),
            url = colDef(show = FALSE),
            state_or_province = colDef(show = FALSE),
            country = colDef(show = FALSE),
            other_routes = colDef(show = FALSE),
            route_color = colDef(show = FALSE),
            st_prov_name = colDef(show = FALSE)
          ))
      )
      }
  )
Show the code
amtrak_table_final <- 
  amtrak_table %>%
  # add title, subtitle, and map
  htmlwidgets::prependContent(
    tags$div(
      tags$link(
        href = "https://fonts.googleapis.com/css?family=Recursive:400,600,700&display=swap", 
        rel = "stylesheet"),
      tags$div(
        tags$div(
          "All Aboard!", 
        style = css(
          'font-size' = '60pt', 
          'font-weight' = 'bold', 
          'font-family' = 'Recursive', 
          'text-align' = 'left',
          'margin-bottom' = 0,
          'padding-left' = '10px',
          'vertical-align' = 'middle')
        ),
        tags$div(
          "Exploring the Amtrak Passenger Rail System", 
          style = css(
            'font-family' = 'Recursive',
            'margin-bottom' = 0,
            'margin-top' = 0,
            'font-size' = '28pt',
            'text-align' = 'left',
            color = '#8C8C8C',
            'padding-left' = '10px')
          ),
        style = css(width = '70%')
      ),
      tags$div(
        plotTag(
          state_route_map,
          alt = "Map of all Amtrak routes",
          height = 200
          ),
        style = css(width = '30%')),
      style = css(
        width = '1250px',
        display = 'inline-flex'))) %>%
  # add footnotes and source notes
  htmlwidgets::appendContent(
    tags$div(
      tags$link(
        href = "https://fonts.googleapis.com/css?family=Recursive:400,600,700&display=swap", 
        rel = "stylesheet"),
      tags$sup("*"), 
      "Amtrak suspended Adirondack service in July 2021, and no resumption date has been set as of October 2022.",
      tags$br(),
      tags$sup("**"), 
      "Berkshire Flyer seasonal service began in 2022, and Valley Flyer service began in 2019.",
      style = css(
        display = 'inline-block',
        'text-align' = 'left',
        'font-family' = 'Recursive',
        color = 'black', 
        'font-size' = '9pt',
        'border-bottom-style' = 'solid',
        'border-top-style' = 'solid',
        width = '1250px',
        'padding-bottom' = '8px',
        'padding-top' = '8px',
        'padding-left' = '10px',
        'border-color' = '#DADADA')),
    tags$div(
      tags$link(
        href = "https://fonts.googleapis.com/css?family=Roboto:400,600,700&display=swap", 
        rel = "stylesheet"),
      tags$div(
        "Data Sources: Wikipedia, US Dept of Transportation, US Census Bureau, TrainWeb.org, and OpenStreetMaps | ",
        style = css(
          display = 'inline-block', 
          'vertical-align' = 'middle')),
      tags$div(
        shiny::icon("twitter"), 
        style = css(
          display = 'inline-block', 
          'vertical-align' = 'middle')),
      tags$div(
        tags$a("@joshfangmeier", href = "https://twitter.com/joshfangmeier", target = "_blank"),
        style = css(
          display = 'inline-block', 
          'vertical-align' = 'middle')),
      tags$div(
        shiny::icon("github"), 
        style = css(
          display = 'inline-block', 
          'vertical-align' = 'middle')),
      tags$div(
        tags$a("jfangmeier", href = "https://github.com/jfangmeier", target = "_blank"), 
        style = css(
          display = 'inline-block', 
          'vertical-align' = 'middle')),
      style = css(
        'text-align' = 'left',
        'font-family' = 'Roboto', 
        color = '#8C8C8C', 
        'font-size' = '10pt', 
        width = '1250px', 
        'padding-top' = '8px', 
        'padding-left' = '10px',
        display = 'inline-block', 
        'vertical-align' = 'middle')
      )
  )

5 Display the table

All Aboard!
Exploring the Amtrak Passenger Rail System
Map of all Amtrak routes
Route
Daily Trips
Passengers (FY 2021)
Distance (miles)
Journey Time
Available Train Cars
Route Map
Northeast Regional
Boston/Springfield-New York-Washington-Norfolk/Newport News/Roanoke
18 (weekday)
15 (weekend)
3,960,000
644 (Newport News)
679 (Norfolk)
682 (Roanoke)
14h 0m
plots
Acela
Boston-New York-Washington
16 (weekday)
4 (Sat)
9 (Sun)
897,600
456
6h 30m
plots
Pacific Surfliner
San Luis Obispo-Goleta-Los Angeles-San Diego
10
841,000
350
8h 30m
plots
Empire Service
New York-Albany-Niagara Falls
7 (Sun-Fri)
6 (Sat)
613,200
460
8h 25m
plots
San Joaquins
Oakland/Sacramento-Bakersfield
6
434,100
318 (Oakland)
280 (Sacramento)
6h 15m (Oakland)
5h 15m (Sacramento)
plots
Keystone Service
New York-Philadelphia-Harrisburg
13 (weekday)
7 (weekend)
394,300
195
3h 7m
plots
Capitol Corridor
Auburn-Sacramento-Oakland-San Jose
9
354,400
172
4h 15m
plots
Lincoln Service
Chicago-St. Louis
4
261,200
284
5h 20m
plots
Maple Leaf
New York-Toronto
1
245,100
544
12h 31m
plots
Hiawatha Service
Chicago-Milwaukee
7 (Mon-Sat)
6 (Sun)
241,600
86
1h 30m
plots
Empire Builder
Chicago-Spokane-Portland/Seattle
1
220,700
2,257 (Portland)
2,206 (Seattle)
45h 0m (Portland)
45h 10m (Seattle)
plots
Downeaster
Brunswick-Portland-Boston
5
205,700
145
2h 25m
plots
Auto Train
Lorton-Sanford
1
199,400
855
17h 30m
plots
Lake Shore Limited
New York/Boston-Albany-Chicago
1
195,900
1,018 (Boston)
959 (New York)
22h 50m (Boston)
19h 0m (New York)
plots
Carolinian
New York-Charlotte
1
194,700
704
12h 55m
plots
Hartford Line
Springfield-New Haven
6 (weekday)
4 (Sat)
5 (Sun)
192,600
63
1h 35m
plots
Coast Starlight
Seattle-Los Angeles
1
189,600
1,377
35h 30m
plots
Silver Star
New York-Miami
1
187,200
1,522
30h 29m
plots
Silver Meteor
New York-Miami
1
187,000
1,389
27h 0m
plots
California Zephyr
Chicago-Emeryville, California
1
184,700
2,438
53h 49m
plots
Amtrak Cascades
Vancouver-Seattle-Portland-Eugene
4
181,500
467
3h 55m (Vancouver)
6h 10m (Eugene)
plots
Wolverine
Chicago-Pontiac
3
153,900
304
6h 19m
plots
Texas Eagle
Chicago-San Antonio
1
151,400
1,306
32h 25m
plots
Illini and Saluki
Chicago-Carbondale
2
150,100
310
5h 30m
plots
Palmetto
New York City-Savannah
1
147,700
829
14h 20m
plots
Southwest Chief
Chicago-Los Angeles
1
135,900
2,256
43h 0m
plots
Pennsylvanian
New York-Pittsburgh
1
128,500
444
9h 10m
plots
Crescent
New York City-New Orleans
1
114,300
1,377
30h 05m
plots
City of New Orleans
Chicago-New Orleans
1
100,800
926
19h 32m
plots
Blue Water
Chicago-Port Huron
1
98,700
319
6h 59m
plots
Capitol Limited
Chicago-Washington
1
96,900
764
17h 35m
plots
Piedmont
Raleigh-Charlotte
3
92,200
173
3h 15m
plots
2
78,200
258
4h 15m
plots
Missouri River Runner
St. Louis-Kansas City
2
77,200
283
5h 40m
plots
Cardinal
Chicago-New York
3 weekly round trips
69,100
1,147
28h 0m
plots
Sunset Limited
New Orleans-Los Angeles
3 weekly round trips
57,600
1,995
67h 55m
plots
Pere Marquette
Chicago-Grand Rapids
1
52,400
176
3h 55m
plots
Heartland Flyer
Oklahoma City-Fort Worth
1
42,300
206
4h 14m
plots
Vermonter
St. Albans-Washington
1
18,600
611
12h 47m
plots
Ethan Allen Express
New York-Burlington
1
12,500
308
7h 35m
plots
Valley Flyer**
Greenfield-Springfield-New Haven
2
NA
98
2h 50m
plots
Berkshire Flyer**
New York - Pittsfield
1 weekly round trip
NA
190
4h 0m
plots
Adirondack*
Montreal-New York
Suspended
NA
381
10h 50m
plots
* Amtrak suspended Adirondack service in July 2021, and no resumption date has been set as of October 2022.
** Berkshire Flyer seasonal service began in 2022, and Valley Flyer service began in 2019.
Data Sources: Wikipedia, US Dept of Transportation, US Census Bureau, TrainWeb.org, and OpenStreetMaps |