|
| 1 | +#' anyplot.ai |
| 2 | +#' flowmap-origin-destination: Origin-Destination Flow Map |
| 3 | +#' Library: ggplot2 3.5.1 | R 4.4.1 |
| 4 | +#' Quality: 81/100 | Created: 2026-05-20 |
| 5 | + |
| 6 | +library(ggplot2) |
| 7 | +library(dplyr) |
| 8 | +library(maps) |
| 9 | +library(ragg) |
| 10 | + |
| 11 | +set.seed(42) |
| 12 | + |
| 13 | +# Theme tokens |
| 14 | +THEME <- Sys.getenv("ANYPLOT_THEME", "light") |
| 15 | +PAGE_BG <- if (THEME == "light") "#FAF8F1" else "#1A1A17" |
| 16 | +ELEVATED_BG <- if (THEME == "light") "#FFFDF6" else "#242420" |
| 17 | +INK <- if (THEME == "light") "#1A1A17" else "#F0EFE8" |
| 18 | +INK_SOFT <- if (THEME == "light") "#4A4A44" else "#B8B7B0" |
| 19 | +OKABE_ITO <- c("#009E73", "#D55E00", "#0072B2", "#CC79A7", |
| 20 | + "#E69F00", "#56B4E9", "#F0E442") |
| 21 | + |
| 22 | +# World basemap polygons for geographic context |
| 23 | +world <- map_data("world") |
| 24 | + |
| 25 | +# Major global air hub coordinates and IATA codes |
| 26 | +airports <- data.frame( |
| 27 | + name = c("New York", "London", "Paris", "Dubai", "Tokyo", |
| 28 | + "Singapore", "Sydney", "Hong Kong", "Amsterdam", "Frankfurt"), |
| 29 | + code = c("NYC", "LHR", "CDG", "DXB", "TYO", |
| 30 | + "SIN", "SYD", "HKG", "AMS", "FRA"), |
| 31 | + lat = c(40.64, 51.47, 49.00, 25.25, 35.55, |
| 32 | + 1.35, -33.95, 22.31, 52.31, 50.03), |
| 33 | + lon = c(-73.78, -0.45, 2.55, 55.36, 139.78, |
| 34 | + 103.99, 151.18, 113.92, 4.76, 8.57), |
| 35 | + region = c("Americas", "Europe", "Europe", "Middle East", "Asia Pacific", |
| 36 | + "Asia Pacific", "Asia Pacific", "Asia Pacific", "Europe", "Europe"), |
| 37 | + stringsAsFactors = FALSE |
| 38 | +) |
| 39 | + |
| 40 | +# Synthetic international air passenger flows (millions per year) |
| 41 | +flows_raw <- data.frame( |
| 42 | + origin = c("New York", "New York", "New York", "London", "London", |
| 43 | + "London", "Dubai", "Dubai", "Dubai", "Singapore", |
| 44 | + "Singapore", "Singapore", "Tokyo", "Frankfurt", "Amsterdam"), |
| 45 | + dest = c("London", "Paris", "Dubai", "Dubai", "Tokyo", |
| 46 | + "Amsterdam", "Singapore", "Frankfurt", "Tokyo", "Hong Kong", |
| 47 | + "Sydney", "Tokyo", "Hong Kong", "Amsterdam", "Paris"), |
| 48 | + flow = c(4.2, 2.8, 3.6, 6.5, 3.1, 2.5, 5.8, 3.4, 2.6, 4.7, |
| 49 | + 2.3, 3.2, 5.1, 3.8, 2.9), |
| 50 | + stringsAsFactors = FALSE |
| 51 | +) |
| 52 | + |
| 53 | +# Join origin and destination coordinates |
| 54 | +flows <- flows_raw |> |
| 55 | + left_join(airports[, c("name", "lat", "lon", "region")], |
| 56 | + by = c("origin" = "name")) |> |
| 57 | + rename(origin_lat = lat, origin_lon = lon, origin_region = region) |> |
| 58 | + left_join(airports[, c("name", "lat", "lon")], |
| 59 | + by = c("dest" = "name")) |> |
| 60 | + rename(dest_lat = lat, dest_lon = lon) |
| 61 | + |
| 62 | +region_colors <- c( |
| 63 | + "Americas" = OKABE_ITO[1], |
| 64 | + "Europe" = OKABE_ITO[2], |
| 65 | + "Middle East" = OKABE_ITO[3], |
| 66 | + "Asia Pacific" = OKABE_ITO[4] |
| 67 | +) |
| 68 | + |
| 69 | +# Manual label nudges to spread the dense European cluster |
| 70 | +airports$nudge_x <- c(-8, -11, -11, 4, 4, 4, 4, 4, -11, 3) |
| 71 | +airports$nudge_y <- c( 2, 4, -3, 2, 2, 2, -3, 2, 1, -4) |
| 72 | + |
| 73 | +p <- ggplot() + |
| 74 | + geom_polygon( |
| 75 | + data = world, |
| 76 | + aes(x = long, y = lat, group = group), |
| 77 | + fill = NA, |
| 78 | + color = INK_SOFT, |
| 79 | + linewidth = 0.15 |
| 80 | + ) + |
| 81 | + geom_curve( |
| 82 | + data = flows, |
| 83 | + aes( |
| 84 | + x = origin_lon, y = origin_lat, |
| 85 | + xend = dest_lon, yend = dest_lat, |
| 86 | + color = origin_region, |
| 87 | + linewidth = flow |
| 88 | + ), |
| 89 | + curvature = -0.3, |
| 90 | + alpha = 0.65, |
| 91 | + arrow = arrow(length = unit(0.006, "npc"), type = "open") |
| 92 | + ) + |
| 93 | + geom_point( |
| 94 | + data = airports, |
| 95 | + aes(x = lon, y = lat, fill = region), |
| 96 | + shape = 21, |
| 97 | + color = PAGE_BG, |
| 98 | + size = 3.5, |
| 99 | + stroke = 0.8 |
| 100 | + ) + |
| 101 | + geom_text( |
| 102 | + data = airports, |
| 103 | + aes(x = lon + nudge_x, y = lat + nudge_y, label = code), |
| 104 | + color = INK, |
| 105 | + size = 2.5, |
| 106 | + fontface = "bold" |
| 107 | + ) + |
| 108 | + scale_color_manual(values = region_colors, name = "Origin Region") + |
| 109 | + scale_fill_manual(values = region_colors, name = "Origin Region") + |
| 110 | + scale_linewidth_continuous(range = c(0.4, 2.8), name = "Flow (M pax/yr)") + |
| 111 | + coord_cartesian(xlim = c(-100, 165), ylim = c(-40, 65)) + |
| 112 | + labs( |
| 113 | + title = "Global Air Passenger Flows · flowmap-origin-destination · r · ggplot2 · anyplot.ai", |
| 114 | + subtitle = "LHR–DXB is the busiest corridor at 6.5M pax/yr", |
| 115 | + x = "Longitude", y = "Latitude" |
| 116 | + ) + |
| 117 | + theme_minimal(base_size = 8) + |
| 118 | + theme( |
| 119 | + plot.background = element_rect(fill = PAGE_BG, color = PAGE_BG), |
| 120 | + panel.background = element_rect(fill = PAGE_BG, color = NA), |
| 121 | + panel.grid.major = element_line(color = INK_SOFT, linewidth = 0.1), |
| 122 | + panel.grid.minor = element_blank(), |
| 123 | + axis.text = element_text(color = INK_SOFT, size = 7), |
| 124 | + axis.title = element_text(color = INK_SOFT, size = 8), |
| 125 | + axis.ticks = element_blank(), |
| 126 | + plot.title = element_text(color = INK, size = 12, face = "bold", |
| 127 | + margin = margin(b = 4)), |
| 128 | + plot.subtitle = element_text(color = INK_SOFT, size = 8, |
| 129 | + margin = margin(b = 6)), |
| 130 | + legend.background = element_rect(fill = ELEVATED_BG, color = INK_SOFT, |
| 131 | + linewidth = 0.3), |
| 132 | + legend.text = element_text(color = INK_SOFT, size = 7), |
| 133 | + legend.title = element_text(color = INK, size = 8), |
| 134 | + legend.key = element_rect(fill = NA, color = NA), |
| 135 | + legend.position = "right", |
| 136 | + plot.margin = margin(t = 8, r = 8, b = 8, l = 8) |
| 137 | + ) + |
| 138 | + guides( |
| 139 | + fill = guide_legend(order = 1), |
| 140 | + color = guide_legend(order = 1), |
| 141 | + linewidth = guide_legend(order = 2) |
| 142 | + ) |
| 143 | + |
| 144 | +ggsave( |
| 145 | + filename = sprintf("plot-%s.png", THEME), |
| 146 | + plot = p, |
| 147 | + device = ragg::agg_png, |
| 148 | + width = 8, |
| 149 | + height = 4.5, |
| 150 | + units = "in", |
| 151 | + dpi = 400 |
| 152 | +) |
0 commit comments