Skip to content

Commit ee34e3b

Browse files
feat(ggplot2): implement hexbin-map-geographic (#7750)
## Implementation: `hexbin-map-geographic` - r/ggplot2 Implements the **r/ggplot2** version of `hexbin-map-geographic`. **File:** `plots/hexbin-map-geographic/implementations/r/ggplot2.R` **Parent Issue:** #3767 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/26514087663)* --------- 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 c3a3ffd commit ee34e3b

2 files changed

Lines changed: 434 additions & 0 deletions

File tree

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
#' anyplot.ai
2+
#' hexbin-map-geographic: Hexagonal Binning Map
3+
#' Library: ggplot2 3.5.1 | R 4.4.1
4+
#' Quality: 89/100 | Created: 2026-05-27
5+
6+
library(ggplot2)
7+
library(dplyr)
8+
library(tibble)
9+
library(scales)
10+
library(ragg)
11+
12+
set.seed(42)
13+
14+
# Theme tokens
15+
THEME <- Sys.getenv("ANYPLOT_THEME", "light")
16+
PAGE_BG <- if (THEME == "light") "#FAF8F1" else "#1A1A17"
17+
ELEVATED_BG <- if (THEME == "light") "#FFFDF6" else "#242420"
18+
INK <- if (THEME == "light") "#1A1A17" else "#F0EFE8"
19+
INK_SOFT <- if (THEME == "light") "#4A4A44" else "#B8B7B0"
20+
MAP_WATER <- if (THEME == "light") "#C8DCE8" else "#1A2530"
21+
MAP_LAND <- if (THEME == "light") "#E6E0D4" else "#2C2A22"
22+
MAP_BORDER <- if (THEME == "light") "#9A9890" else "#58584E"
23+
24+
# NYC metro area taxi pickups — simulated density clusters
25+
# Centres: Midtown, Greenwich Village, UWS, Chelsea, S.Brooklyn,
26+
# Midtown East, Lower Manhattan, JFK-adjacent, LGA-adjacent, Jersey City
27+
cluster_centers <- tibble::tibble(
28+
lat = c(40.756, 40.730, 40.775, 40.744, 40.676,
29+
40.762, 40.710, 40.645, 40.769, 40.727),
30+
lon = c(-73.989, -74.003, -73.981, -73.995, -73.982,
31+
-73.971, -73.997, -73.794, -73.863, -74.077),
32+
n = c(5000L, 3200L, 2400L, 2600L, 1800L,
33+
1400L, 1000L, 700L, 600L, 500L),
34+
s_lat = c(0.010, 0.009, 0.011, 0.009, 0.013,
35+
0.010, 0.009, 0.009, 0.008, 0.009),
36+
s_lon = c(0.013, 0.011, 0.013, 0.011, 0.015,
37+
0.012, 0.011, 0.012, 0.010, 0.011)
38+
)
39+
40+
pickups <- dplyr::bind_rows(
41+
lapply(seq_len(nrow(cluster_centers)), function(i) {
42+
cc <- cluster_centers[i, ]
43+
tibble::tibble(
44+
lat = rnorm(cc$n, cc$lat, cc$s_lat),
45+
lon = rnorm(cc$n, cc$lon, cc$s_lon)
46+
)
47+
})
48+
) |> dplyr::filter(
49+
lat >= 40.60, lat <= 40.83,
50+
lon >= -74.15, lon <= -73.75
51+
)
52+
53+
# --- Simplified NYC geographic context (hand-digitized approximate outlines) ---
54+
# sf/rnaturalearth not available; inline polygons provide coastline context.
55+
# Water shows as panel background (MAP_WATER).
56+
57+
# New Jersey — land mass west of the Hudson River
58+
nj_land <- tibble::tibble(
59+
group = "nj",
60+
lon = c(-74.15, -74.15, -73.960, -73.980, -74.015, -74.034, -74.050,
61+
-74.085, -74.115, -74.15),
62+
lat = c(40.60, 40.83, 40.830, 40.800, 40.765, 40.725, 40.700,
63+
40.660, 40.630, 40.60)
64+
)
65+
66+
# Manhattan Island — narrow north-south strip between Hudson and East River.
67+
# Points trace the eastern shoreline (Battery → Harlem) then west (Harlem → Battery).
68+
manhattan_land <- tibble::tibble(
69+
group = "manhattan",
70+
lon = c(
71+
# East shore: south to north
72+
-73.971, -73.972, -73.975, -73.971, -73.965, -73.952, -73.940, -73.928,
73+
# North clip at lat 40.830 (Inwood not in view)
74+
-73.934, -73.943,
75+
# West shore: north to south
76+
-73.955, -73.965, -73.973, -73.982, -73.991, -74.001, -74.010, -74.014, -73.971
77+
),
78+
lat = c(
79+
40.700, 40.725, 40.742, 40.758, 40.764, 40.788, 40.808, 40.830,
80+
40.830, 40.822,
81+
40.813, 40.800, 40.782, 40.764, 40.749, 40.732, 40.715, 40.703, 40.700
82+
)
83+
)
84+
85+
# Brooklyn and Queens — southeastern land mass south of the East River
86+
bq_land <- tibble::tibble(
87+
group = "bq",
88+
lon = c(-74.030, -73.993, -73.975, -73.960, -73.938, -73.880, -73.800,
89+
-73.75, -73.75, -74.030),
90+
lat = c(40.700, 40.698, 40.684, 40.672, 40.665, 40.651, 40.636,
91+
40.628, 40.60, 40.60)
92+
)
93+
94+
nyc_geo <- dplyr::bind_rows(nj_land, manhattan_land, bq_land)
95+
96+
# --- Plot ---
97+
98+
plot_title <- "hexbin-map-geographic · r · ggplot2 · anyplot.ai"
99+
title_size <- max(8L, round(12 * min(1.0, 67 / nchar(plot_title))))
100+
101+
p <- ggplot() +
102+
# Geographic base map
103+
geom_polygon(
104+
data = nyc_geo,
105+
aes(x = lon, y = lat, group = group),
106+
fill = MAP_LAND,
107+
color = MAP_BORDER,
108+
linewidth = 0.30
109+
) +
110+
# Hexbin density layer
111+
geom_hex(
112+
data = pickups,
113+
aes(x = lon, y = lat),
114+
bins = 38,
115+
alpha = 0.88
116+
) +
117+
# Storytelling annotation: point to primary hotspot
118+
annotate("segment",
119+
x = -73.975, xend = -73.989,
120+
y = 40.773, yend = 40.762,
121+
color = INK, linewidth = 0.45,
122+
arrow = arrow(length = unit(0.18, "cm"), type = "closed")
123+
) +
124+
annotate("text",
125+
x = -73.968, y = 40.776,
126+
label = "Midtown hotspot",
127+
color = INK,
128+
size = 2.7,
129+
fontface = "italic",
130+
hjust = 0
131+
) +
132+
scale_fill_gradient(
133+
low = "#009E73",
134+
high = "#4467A3",
135+
name = "Pickup\nCount",
136+
labels = scales::comma,
137+
guide = guide_colorbar(barwidth = 0.8, barheight = 8)
138+
) +
139+
scale_x_continuous(
140+
labels = function(x) sprintf("%.2f°W", -x),
141+
expand = expansion(mult = 0.0)
142+
) +
143+
scale_y_continuous(
144+
labels = function(y) sprintf("%.2f°N", y),
145+
expand = expansion(mult = 0.0)
146+
) +
147+
coord_fixed(
148+
ratio = 1,
149+
xlim = c(-74.15, -73.75),
150+
ylim = c(40.60, 40.83)
151+
) +
152+
labs(
153+
x = "Longitude",
154+
y = "Latitude",
155+
title = plot_title
156+
) +
157+
theme_minimal(base_size = 8) +
158+
theme(
159+
plot.background = element_rect(fill = PAGE_BG, color = PAGE_BG),
160+
panel.background = element_rect(fill = MAP_WATER, color = NA),
161+
panel.grid.major = element_line(color = INK_SOFT, linewidth = 0.10),
162+
panel.grid.minor = element_blank(),
163+
axis.title = element_text(color = INK, size = 10),
164+
axis.text = element_text(color = INK_SOFT, size = 8),
165+
plot.title = element_text(color = INK, size = title_size, face = "bold"),
166+
legend.background = element_rect(fill = ELEVATED_BG, color = INK_SOFT, linewidth = 0.3),
167+
legend.text = element_text(color = INK_SOFT, size = 8),
168+
legend.title = element_text(color = INK, size = 9),
169+
legend.position = "right",
170+
plot.margin = margin(0.4, 0.4, 0.4, 0.4, "cm")
171+
)
172+
173+
ggsave(
174+
filename = sprintf("plot-%s.png", THEME),
175+
plot = p,
176+
device = ragg::agg_png,
177+
width = 8,
178+
height = 4.5,
179+
units = "in",
180+
dpi = 400
181+
)

0 commit comments

Comments
 (0)