Skip to content

Commit 61ac100

Browse files
feat(ggplot2): implement flowmap-origin-destination (#7504)
## Implementation: `flowmap-origin-destination` - r/ggplot2 Implements the **r/ggplot2** version of `flowmap-origin-destination`. **File:** `plots/flowmap-origin-destination/implementations/r/ggplot2.R` **Parent Issue:** #3765 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/26154296171)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent b36139d commit 61ac100

2 files changed

Lines changed: 400 additions & 0 deletions

File tree

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
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

Comments
 (0)