Connection Lines Map (Origin-Destination) — ggplot2

A geographic map visualization showing connection lines (arcs or great circles) between pairs of locations. This plot excels at revealing spatial patterns in flows, routes, and relationships between places. The curved lines naturally represent shortest paths on Earth's surface while avoiding visual overlap with straight lines. Line properties like thickness or color can encode additional variables such as traffic volume or connection type.

Connection Lines Map (Origin-Destination) rendered with ggplot2

Renders

R source (ggplot2)

#' anyplot.ai
#' map-connection-lines: Connection Lines Map (Origin-Destination)
#' Library: ggplot2 3.5.1 | R 4.4.1
#' Quality: 85/100 | Created: 2026-05-28

library(ggplot2)
library(dplyr)
library(ragg)
if (!requireNamespace("maps", quietly = TRUE)) install.packages("maps", repos = "https://cran.r-project.org")

set.seed(42)

# Theme tokens
THEME       <- Sys.getenv("ANYPLOT_THEME", "light")
PAGE_BG     <- if (THEME == "light") "#FAF8F1" else "#1A1A17"
ELEVATED_BG <- if (THEME == "light") "#FFFDF6" else "#242420"
INK         <- if (THEME == "light") "#1A1A17" else "#F0EFE8"
INK_SOFT    <- if (THEME == "light") "#4A4A44" else "#B8B7B0"
INK_MUTED   <- if (THEME == "light") "#6B6A63" else "#A8A79F"
IMPRINT_PALETTE <- c("#009E73", "#C475FD", "#4467A3", "#BD8233",
                     "#AE3030", "#2ABCCD", "#954477", "#99B314")

# Helper: spherical linear interpolation along a great circle arc
great_circle_arc <- function(lon1, lat1, lon2, lat2, n = 80L) {
  to_rad <- function(x) x * pi / 180
  to_deg <- function(x) x * 180 / pi

  lon1r <- to_rad(lon1); lat1r <- to_rad(lat1)
  lon2r <- to_rad(lon2); lat2r <- to_rad(lat2)

  x1 <- cos(lat1r) * cos(lon1r); y1 <- cos(lat1r) * sin(lon1r); z1 <- sin(lat1r)
  x2 <- cos(lat2r) * cos(lon2r); y2 <- cos(lat2r) * sin(lon2r); z2 <- sin(lat2r)

  d <- acos(pmax(pmin(x1 * x2 + y1 * y2 + z1 * z2, 1.0), -1.0))
  if (d < 1e-9) return(data.frame(lon = lon1, lat = lat1))

  t  <- seq(0, 1, length.out = n)
  xa <- (sin((1 - t) * d) * x1 + sin(t * d) * x2) / sin(d)
  ya <- (sin((1 - t) * d) * y1 + sin(t * d) * y2) / sin(d)
  za <- (sin((1 - t) * d) * z1 + sin(t * d) * z2) / sin(d)

  data.frame(
    lon = to_deg(atan2(ya, xa)),
    lat = to_deg(atan2(za, sqrt(xa^2 + ya^2)))
  )
}

# Data: major international hub airports
airports <- data.frame(
  city = c("New York", "London",    "Tokyo",      "Dubai",
           "Sydney",   "Sao Paulo", "Singapore",  "Cape Town",
           "Mumbai",   "Los Angeles","Frankfurt", "Hong Kong"),
  lat  = c( 40.64,  51.48,  35.55,  25.25,
            -33.95, -23.43,   1.36, -33.96,
             19.09,  33.94,  50.03,  22.31),
  lon  = c(-73.78,  -0.45, 139.78,  55.36,
           151.17, -46.47, 103.99,  18.60,
            72.87, -118.41,  8.57, 113.91),
  stringsAsFactors = FALSE
)

# Label nudge direction (avoid overlaps at map boundaries)
airports$hjust <- c(1.1, 1.1, -0.1, -0.1,
                    -0.1,  1.1, -0.1,  1.1,
                    -0.1,  1.1,  1.1, -0.1)
airports$vjust <- c(0.5, 1.5, 0.5, 0.5,
                    0.5, 0.5, 1.5, 0.5,
                    0.5, 1.5, -0.5, 1.5)

# Routes: international flight connections with annual passengers (millions)
routes_raw <- data.frame(
  origin       = c("New York",  "London",    "Dubai",     "Singapore", "Los Angeles",
                   "Frankfurt", "New York",  "London",    "Dubai",     "Sao Paulo",
                   "Tokyo",     "Hong Kong", "Mumbai",    "Sydney",    "Cape Town"),
  dest         = c("London",    "Dubai",     "Mumbai",    "Tokyo",     "Tokyo",
                   "Dubai",     "Los Angeles","Frankfurt", "Cape Town", "London",
                   "Los Angeles","Singapore","Dubai",     "Los Angeles","Dubai"),
  passengers_m = c(3.2, 4.8, 3.1, 2.9, 5.1,
                   3.6, 4.3, 2.1, 1.9, 1.4,
                   4.7, 3.8, 2.6, 2.3, 1.1),
  stringsAsFactors = FALSE
)

# Join airport coordinates
routes <- routes_raw |>
  left_join(airports[, c("city", "lat", "lon")], by = c("origin" = "city")) |>
  rename(origin_lat = lat, origin_lon = lon) |>
  left_join(airports[, c("city", "lat", "lon")], by = c("dest" = "city")) |>
  rename(dest_lat = lat, dest_lon = lon)

# Build arc segments — split at antimeridian to avoid horizontal line artifacts
all_segs    <- list()
seg_counter <- 1L

for (i in seq_len(nrow(routes))) {
  row <- routes[i, ]
  pts <- great_circle_arc(row$origin_lon, row$origin_lat,
                          row$dest_lon,   row$dest_lat)
  pts$passengers <- row$passengers_m

  jump_pos <- which(abs(diff(pts$lon)) > 180)

  if (length(jump_pos) == 0L) {
    pts$seg_id      <- seg_counter
    seg_counter     <- seg_counter + 1L
    all_segs[[length(all_segs) + 1]] <- pts
  } else {
    bounds <- c(0L, jump_pos, nrow(pts))
    for (j in seq_len(length(bounds) - 1L)) {
      seg        <- pts[(bounds[j] + 1L):bounds[j + 1L], ]
      seg$seg_id <- seg_counter
      seg_counter <- seg_counter + 1L
      all_segs[[length(all_segs) + 1]] <- seg
    }
  }
}

arc_df <- do.call(rbind, all_segs)

# Graticule grid lines (every 30°)
graticule_h <- data.frame(lat = seq(-60, 60, by = 30))
graticule_v <- data.frame(lon = seq(-180, 180, by = 60))

# Title with font-size scaling
title_str  <- "Global Flight Routes · map-connection-lines · r · ggplot2 · anyplot.ai"
title_size <- max(round(12 * 67 / nchar(title_str)), 8L)

world_map <- map_data("world")

p <- ggplot() +
  # Base map: country borders for geographic context
  geom_polygon(
    data    = world_map,
    mapping = aes(x = long, y = lat, group = group),
    fill    = INK_MUTED,
    color   = NA,
    alpha   = 0.15
  ) +
  # Horizontal graticule lines
  geom_hline(
    data    = graticule_h,
    mapping = aes(yintercept = lat),
    color   = INK_SOFT,
    linewidth = 0.1,
    alpha   = 0.25
  ) +
  # Vertical graticule lines
  geom_vline(
    data    = graticule_v,
    mapping = aes(xintercept = lon),
    color   = INK_SOFT,
    linewidth = 0.1,
    alpha   = 0.25
  ) +
  # Flight route arcs colored and weighted by passenger volume
  geom_path(
    data    = arc_df,
    mapping = aes(x = lon, y = lat, group = seg_id,
                  color = passengers, linewidth = passengers),
    alpha   = 0.60,
    lineend = "round"
  ) +
  # Airport markers (hollow circles, brand green)
  geom_point(
    data    = airports,
    mapping = aes(x = lon, y = lat),
    shape   = 21,
    size    = 2.5,
    fill    = IMPRINT_PALETTE[1],
    color   = PAGE_BG,
    stroke  = 0.9
  ) +
  # City labels for geographic context
  geom_text(
    data    = airports,
    mapping = aes(x = lon, y = lat, label = city,
                  hjust = hjust, vjust = vjust),
    color   = INK_SOFT,
    size    = 2.4,
    fontface = "plain"
  ) +
  scale_color_gradient(
    low   = "#009E73",
    high  = "#4467A3",
    name  = "Passengers\n(millions)",
    guide = guide_colorbar(
      barwidth       = 0.7,
      barheight      = 5.0,
      title.position = "top",
      ticks.colour   = INK_SOFT
    )
  ) +
  scale_linewidth_continuous(range = c(0.5, 2.6), guide = "none") +
  scale_x_continuous(
    limits = c(-180, 180),
    breaks = seq(-120, 120, by = 60),
    labels = function(x) paste0(abs(x), ifelse(x < 0, "°W", ifelse(x > 0, "°E", "°")))
  ) +
  scale_y_continuous(
    limits = c(-65, 80),
    breaks = seq(-60, 60, by = 30),
    labels = function(y) paste0(abs(y), ifelse(y < 0, "°S", ifelse(y > 0, "°N", "°")))
  ) +
  labs(title = title_str, x = NULL, y = NULL) +
  theme_minimal(base_size = 8) +
  theme(
    plot.background  = element_rect(fill = PAGE_BG, color = PAGE_BG),
    panel.background = element_rect(fill = PAGE_BG, color = NA),
    panel.grid.major = element_blank(),
    panel.grid.minor = element_blank(),
    panel.border     = element_rect(color = INK_SOFT, fill = NA, linewidth = 0.3),
    axis.text        = element_text(color = INK_SOFT, size = 7),
    axis.title       = element_blank(),
    axis.ticks       = element_blank(),
    plot.title       = element_text(
      color  = INK,
      size   = title_size,
      hjust  = 0.5,
      margin = margin(b = 10)
    ),
    legend.background = element_rect(fill = ELEVATED_BG, color = NA),
    legend.text       = element_text(color = INK_SOFT, size = 8),
    legend.title      = element_text(color = INK, size = 9),
    legend.position   = "right",
    plot.margin       = margin(8, 12, 8, 8)
  )

ggsave(
  filename = sprintf("plot-%s.png", THEME),
  plot     = p,
  device   = ragg::agg_png,
  width    = 8,
  height   = 4.5,
  units    = "in",
  dpi      = 400
)

Part of Connection Lines Map (Origin-Destination) on anyplot.ai.

Other implementations