#30DayMapChallenge Day 23: Process - Interactive flow map with odbr and flowmapper

Author

Jorge Ubirajara

This tutorial documents how I build an interactive desire lines map for the São Paulo Metropolitan Area (2017) using odbr for OD survey data and flowmapper for the flow network, with dplyr, ggplot2, sf, and plotly for wrangling and visualization.

1. Setup

We load the required packages. Uncomment install lines if needed.

Code
# install.packages(c("tidyverse","sf","plotly","geobr","odbr","flowmapper"))
library(tidyverse)
library(sf)
library(plotly)
library(geobr)
library(odbr)
library(flowmapper)

sf::sf_use_s2(TRUE)

2. Data inputs (odbr)

Generating three inputs from odbr: (i) trip table with expansion factor fe_via; (ii) traffic zones; and (iii) municipal boundaries:

Code
# Trip table for São Paulo, 2017
rmsp <- odbr::read_od(city = "São Paulo", year = 2017)
Downloading file to: C:\Users\jorge\AppData\Local\Temp\RtmpWiRgqH/od_sao_paulo_2017_not_harmonized.csv.gz
Code
# Traffic zone polygons
zonas_rmsp <- odbr::read_map(city = "São Paulo", year = 2017, geometry = "zone")
Downloading file to: C:\Users\jorge\AppData\Local\Temp\RtmpWiRgqH/zone_sao_paulo_2017_not_harmonized.gpkg
Code
# Municipal boundaries
rmsp_geo <- odbr::read_map(city = "São Paulo", year = 2017, geometry = "municipality")
Downloading file to: C:\Users\jorge\AppData\Local\Temp\RtmpWiRgqH/municipality_sao_paulo_2017_not_harmonized.gpkg

3. Zone centroids and node table (fixed)

Since flowmapper expects a node table with name, x, and y., we compute centroids on surface and extract their coordinates:

Code
cent_zonas_rmsp <- st_point_on_surface(zonas_rmsp) %>% 
  mutate(x = st_coordinates(.)[,1],
         y = st_coordinates(.)[,2]) %>% 
  rename('name' = 'NumeroZona')
Warning: st_point_on_surface assumes attributes are constant over geometries

4. Aggregate OD flows with expansion factors

We aggregate trips by origin and destination, summing the expansion factors (fe_via) from all trips between 05:00 AM and 09:00 AM to obtain total flows. Then then rename to the columns expected by flowmapper.

Code
od_rmsp <- rmsp  %>% 
  filter(h_saida >= 5 & h_saida <= 9) %>% 
  group_by(zona_o, zona_d)  %>% 
  summarize(n_via = sum(fe_via, na.rm = TRUE), .groups = "drop") %>%   arrange(desc(n_via))

od_flows <- od_rmsp %>% 
  rename(o = zona_o, d = zona_d, value = n_via)

Filtering out small flows improves readability of the network:

Code
min_flow <- 2000   # adjust to taste
od_flows_filt <- od_flows |>
  filter(value >= min_flow)

5. Basemap with ggplot2

Simple zonal outline and municipal boundaries for orientation.

Code
p_base <- ggplot() +
  geom_sf(data = zonas_rmsp, fill = NA, color = "gray30", linewidth = 0.1) +
  geom_sf(data = rmsp_geo, fill = NA, color = "darkblue", linewidth = 1) +
  geom_sf_text(data = rmsp_geo, aes(label = NomeMunici), size = 2) +
  theme_void()

p_base

6. Desire lines with flowmapper package

Key controls: - node_radius_factor controls node symbol size - edge_width_factor controls edge thickness - edge_offset_factor allows parallel offsets if desired

Code
p_flow <- add_flowmap(
  p = p_base,
  od = od_flows_filt,
  nodes = cent_zonas_rmsp,
  node_radius_factor = 0.5,
  edge_width_factor  = 0.2,
  edge_offset_factor = 0.0
) +
  scale_fill_gradient(
    name = "# Trips",
    low  = "white",
    high = "darkred"
  )
Warning in prep_flowmap(flowdat = flowdat, od = od, nodes = nodes, k_nodes =
k_nodes, : Number of flows very high. Consider setting k_nodes to cluster
nodes.
Code
p_flow

7. Interactivity with plotly

ggplotly converts the map into an interactive figure. Tooltips show OD and value.

Code
p_interactive <- ggplotly(p_flow, tooltip = c("o", "d", "value"))
p_interactive